github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/projectfile/projectfile.go (about) 1 package projectfile 2 3 import ( 4 "errors" 5 "fmt" 6 "net/url" 7 "os" 8 "os/user" 9 "path/filepath" 10 "regexp" 11 "runtime" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/ActiveState/cli/internal/assets" 17 "github.com/ActiveState/cli/internal/condition" 18 "github.com/ActiveState/cli/internal/config" 19 "github.com/ActiveState/cli/internal/constants" 20 "github.com/ActiveState/cli/internal/errs" 21 "github.com/ActiveState/cli/internal/fileutils" 22 "github.com/ActiveState/cli/internal/hash" 23 "github.com/ActiveState/cli/internal/language" 24 "github.com/ActiveState/cli/internal/locale" 25 "github.com/ActiveState/cli/internal/logging" 26 "github.com/ActiveState/cli/internal/multilog" 27 "github.com/ActiveState/cli/internal/osutils" 28 "github.com/ActiveState/cli/internal/profile" 29 "github.com/ActiveState/cli/internal/rollbar" 30 "github.com/ActiveState/cli/internal/rtutils" 31 "github.com/ActiveState/cli/internal/sliceutils" 32 "github.com/ActiveState/cli/internal/strutils" 33 "github.com/ActiveState/cli/pkg/sysinfo" 34 "github.com/go-openapi/strfmt" 35 "github.com/google/uuid" 36 "github.com/imdario/mergo" 37 "github.com/spf13/cast" 38 "github.com/thoas/go-funk" 39 "gopkg.in/yaml.v2" 40 ) 41 42 var ( 43 urlProjectRegexStr = `https:\/\/[\w\.]+\/([\w_.-]*)\/([\w_.-]*)(?:\?commitID=)*([^&]*)(?:\&branch=)*(.*)` 44 urlCommitRegexStr = `https:\/\/[\w\.]+\/commit\/(.*)` 45 46 // ProjectURLRe Regex used to validate project fields /orgname/projectname[?commitID=someUUID] 47 ProjectURLRe = regexp.MustCompile(urlProjectRegexStr) 48 // CommitURLRe Regex used to validate commit info /commit/someUUID 49 CommitURLRe = regexp.MustCompile(urlCommitRegexStr) 50 // deprecatedRegex covers the deprecated fields in the project file 51 deprecatedRegex = regexp.MustCompile(`(?m)^\s*(?:constraints|platforms|languages):`) 52 // nonAlphanumericRegex covers all non alphanumeric characters 53 nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9 ]+`) 54 ) 55 56 const ConfigVersion = 1 57 58 type MigratorFunc func(project *Project, configVersion int) (int, error) 59 60 var migrationRunning bool 61 62 var migrator MigratorFunc 63 64 func RegisterMigrator(m MigratorFunc) { 65 migrator = m 66 } 67 68 type ErrorParseProject struct{ *locale.LocalizedError } 69 70 type ErrorNoProject struct{ *locale.LocalizedError } 71 72 type ErrorNoProjectFromEnv struct{ *locale.LocalizedError } 73 74 type ErrorNoDefaultProject struct{ *locale.LocalizedError } 75 76 // projectURL comprises all fields of a parsed project URL 77 type projectURL struct { 78 Owner string 79 Name string 80 LegacyCommitID string 81 BranchName string 82 } 83 84 const LocalProjectsConfigKey = "projects" 85 86 // VersionInfo is used in cases where we only care about parsing the version and channel fields. 87 // In all other cases the version is parsed via the Project struct 88 type VersionInfo struct { 89 Channel string `yaml:"branch"` // branch for backward compatibility 90 Version string 91 Lock string `yaml:"lock"` 92 } 93 94 // ProjectSimple reflects a bare basic project structure 95 type ProjectSimple struct { 96 Project string `yaml:"project"` 97 } 98 99 // Project covers the top level project structure of our yaml 100 type Project struct { 101 Project string `yaml:"project"` 102 ConfigVersion int `yaml:"config_version"` 103 Lock string `yaml:"lock,omitempty"` 104 Environments string `yaml:"environments,omitempty"` 105 Constants Constants `yaml:"constants,omitempty"` 106 Secrets *SecretScopes `yaml:"secrets,omitempty"` 107 Events Events `yaml:"events,omitempty"` 108 Scripts Scripts `yaml:"scripts,omitempty"` 109 Jobs Jobs `yaml:"jobs,omitempty"` 110 Private bool `yaml:"private,omitempty"` 111 Cache string `yaml:"cache,omitempty"` 112 path string // "private" 113 parsedURL projectURL // parsed url data 114 parsedChannel string 115 parsedVersion string 116 } 117 118 // Build covers the build map, which can go under languages or packages 119 // Build can hold variable keys, so we cannot predict what they are, hence why it is a map 120 type Build map[string]string 121 122 // ConstantFields are the common fields for the Constant type. This is required 123 // for type composition related to its yaml.Unmarshaler implementation. 124 type ConstantFields struct { 125 Conditional Conditional `yaml:"if,omitempty"` 126 } 127 128 // Constant covers the constant structure, which goes under Project 129 type Constant struct { 130 NameVal `yaml:",inline"` 131 ConstantFields `yaml:",inline"` 132 } 133 134 func (c *Constant) UnmarshalYAML(unmarshal func(interface{}) error) error { 135 if err := unmarshal(&c.NameVal); err != nil { 136 return err 137 } 138 if err := unmarshal(&c.ConstantFields); err != nil { 139 return err 140 } 141 return nil 142 } 143 144 var _ ConstrainedEntity = &Constant{} 145 146 // ID returns the constant name 147 func (c *Constant) ID() string { 148 return c.Name 149 } 150 151 func (c *Constant) ConditionalFilter() Conditional { 152 return c.Conditional 153 } 154 155 // Constants is a slice of constant values 156 type Constants []*Constant 157 158 // AsConstrainedEntities boxes constants as a slice ConstrainedEntities 159 func (constants Constants) AsConstrainedEntities() (items []ConstrainedEntity) { 160 for _, c := range constants { 161 items = append(items, c) 162 } 163 return items 164 } 165 166 // MakeConstantsFromConstrainedEntities unboxes ConstraintedEntities as Constants 167 func MakeConstantsFromConstrainedEntities(items []ConstrainedEntity) (constants []*Constant) { 168 constants = make([]*Constant, 0, len(items)) 169 for _, v := range items { 170 if o, ok := v.(*Constant); ok { 171 constants = append(constants, o) 172 } 173 } 174 return constants 175 } 176 177 // SecretScopes holds secret scopes, scopes define what the secrets belong to 178 type SecretScopes struct { 179 User Secrets `yaml:"user,omitempty"` 180 Project Secrets `yaml:"project,omitempty"` 181 } 182 183 // Secret covers the variable structure, which goes under Project 184 type Secret struct { 185 Name string `yaml:"name"` 186 Description string `yaml:"description"` 187 Conditional Conditional `yaml:"if,omitempty"` 188 } 189 190 var _ ConstrainedEntity = &Secret{} 191 192 // ID returns the secret name 193 func (s *Secret) ID() string { 194 return s.Name 195 } 196 197 func (s *Secret) ConditionalFilter() Conditional { 198 return s.Conditional 199 } 200 201 // Secrets is a slice of Secret definitions 202 type Secrets []*Secret 203 204 // AsConstrainedEntities box Secrets as a slice of ConstrainedEntities 205 func (secrets Secrets) AsConstrainedEntities() (items []ConstrainedEntity) { 206 for _, s := range secrets { 207 items = append(items, s) 208 } 209 return items 210 } 211 212 // MakeSecretsFromConstrainedEntities unboxes ConstraintedEntities as Secrets 213 func MakeSecretsFromConstrainedEntities(items []ConstrainedEntity) (secrets []*Secret) { 214 secrets = make([]*Secret, 0, len(items)) 215 for _, v := range items { 216 if o, ok := v.(*Secret); ok { 217 secrets = append(secrets, o) 218 } 219 } 220 return secrets 221 } 222 223 // Conditional is an `if` conditional that when evalutes to true enables the entity its under 224 // it is meant to replace Constraints 225 type Conditional string 226 227 // ConstrainedEntity is an entity in a project file that can be filtered with constraints 228 type ConstrainedEntity interface { 229 // ID returns the name of the entity 230 ID() string 231 232 ConditionalFilter() Conditional 233 } 234 235 // Package covers the package structure, which goes under the language struct 236 type Package struct { 237 Name string `yaml:"name"` 238 Version string `yaml:"version"` 239 Conditional Conditional `yaml:"if,omitempty"` 240 Build Build `yaml:"build,omitempty"` 241 } 242 243 var _ ConstrainedEntity = Package{} 244 245 // ID returns the package name 246 func (p Package) ID() string { 247 return p.Name 248 } 249 250 func (p Package) ConditionalFilter() Conditional { 251 return p.Conditional 252 } 253 254 // Packages is a slice of Package configurations 255 type Packages []Package 256 257 // AsConstrainedEntities boxes Packages as a slice of ConstrainedEntities 258 func (packages Packages) AsConstrainedEntities() (items []ConstrainedEntity) { 259 for i := range packages { 260 items = append(items, &packages[i]) 261 } 262 return items 263 } 264 265 // MakePackagesFromConstrainedEntities unboxes ConstraintedEntities as Packages 266 func MakePackagesFromConstrainedEntities(items []ConstrainedEntity) (packages []*Package) { 267 packages = make([]*Package, 0, len(items)) 268 for _, v := range items { 269 if o, ok := v.(*Package); ok { 270 packages = append(packages, o) 271 } 272 } 273 return packages 274 } 275 276 // EventFields are the common fields for the Event type. This is required 277 // for type composition related to its yaml.Unmarshaler implementation. 278 type EventFields struct { 279 Scope []string `yaml:"scope"` 280 Conditional Conditional `yaml:"if,omitempty"` 281 id string 282 } 283 284 // Event covers the event structure, which goes under Project 285 type Event struct { 286 NameVal `yaml:",inline"` 287 EventFields `yaml:",inline"` 288 } 289 290 func (e *Event) UnmarshalYAML(unmarshal func(interface{}) error) error { 291 if err := unmarshal(&e.NameVal); err != nil { 292 return err 293 } 294 if err := unmarshal(&e.EventFields); err != nil { 295 return err 296 } 297 return nil 298 } 299 300 var _ ConstrainedEntity = Event{} 301 302 // ID returns the event name 303 func (e Event) ID() string { 304 if e.id == "" { 305 id, err := uuid.NewUUID() 306 if err != nil { 307 multilog.Error("UUID generation failed, defaulting to serialization") 308 e.id = hash.ShortHash(e.Name, e.Value, strings.Join(e.Scope, "")) 309 } else { 310 e.id = id.String() 311 } 312 } 313 return e.id 314 } 315 316 func (e Event) ConditionalFilter() Conditional { 317 return e.Conditional 318 } 319 320 // Events is a slice of Event definitions 321 type Events []Event 322 323 // AsConstrainedEntities boxes events as a slice of ConstrainedEntities 324 func (events Events) AsConstrainedEntities() (items []ConstrainedEntity) { 325 for i := range events { 326 items = append(items, &events[i]) 327 } 328 return items 329 } 330 331 // MakeEventsFromConstrainedEntities unboxes ConstraintedEntities as Events 332 func MakeEventsFromConstrainedEntities(items []ConstrainedEntity) (events []*Event) { 333 events = make([]*Event, 0, len(items)) 334 for _, v := range items { 335 if o, ok := v.(*Event); ok { 336 events = append(events, o) 337 } 338 } 339 return events 340 } 341 342 // ScriptFields are the common fields for the Script type. This is required 343 // for type composition related to its yaml.Unmarshaler implementation. 344 type ScriptFields struct { 345 Description string `yaml:"description,omitempty"` 346 Filename string `yaml:"filename,omitempty"` 347 Standalone bool `yaml:"standalone,omitempty"` 348 Language string `yaml:"language,omitempty"` 349 Conditional Conditional `yaml:"if,omitempty"` 350 } 351 352 // Script covers the script structure, which goes under Project 353 type Script struct { 354 NameVal `yaml:",inline"` 355 ScriptFields `yaml:",inline"` 356 } 357 358 func (s *Script) UnmarshalYAML(unmarshal func(interface{}) error) error { 359 if err := unmarshal(&s.NameVal); err != nil { 360 return err 361 } 362 if err := unmarshal(&s.ScriptFields); err != nil { 363 return err 364 } 365 return nil 366 } 367 368 var _ ConstrainedEntity = Script{} 369 370 // ID returns the script name 371 func (s Script) ID() string { 372 return s.Name 373 } 374 375 func (s Script) ConditionalFilter() Conditional { 376 return s.Conditional 377 } 378 379 // Scripts is a slice of scripts 380 type Scripts []Script 381 382 // AsConstrainedEntities boxes scripts as a slice of ConstrainedEntities 383 func (scripts Scripts) AsConstrainedEntities() (items []ConstrainedEntity) { 384 for i := range scripts { 385 items = append(items, &scripts[i]) 386 } 387 return items 388 } 389 390 // MakeScriptsFromConstrainedEntities unboxes ConstraintedEntities as Scripts 391 func MakeScriptsFromConstrainedEntities(items []ConstrainedEntity) (scripts []*Script) { 392 scripts = make([]*Script, 0, len(items)) 393 for _, v := range items { 394 if o, ok := v.(*Script); ok { 395 scripts = append(scripts, o) 396 } 397 } 398 return scripts 399 } 400 401 // Job covers the job structure, which goes under Project 402 type Job struct { 403 Name string `yaml:"name"` 404 Constants []string `yaml:"constants"` 405 Scripts []string `yaml:"scripts"` 406 } 407 408 // Jobs is a slice of jobs 409 type Jobs []Job 410 411 var persistentProject *Project 412 413 // Parse the given filepath, which should be the full path to an activestate.yaml file 414 func Parse(configFilepath string) (_ *Project, rerr error) { 415 projectDir := filepath.Dir(configFilepath) 416 files, err := os.ReadDir(projectDir) 417 if err != nil { 418 return nil, locale.WrapError(err, "err_project_readdir", "Could not read project directory: {{.V0}}.", projectDir) 419 } 420 421 project, err := parse(configFilepath) 422 if err != nil { 423 return nil, err 424 } 425 426 re, _ := regexp.Compile(`activestate[._-](\w+)\.yaml`) 427 for _, file := range files { 428 match := re.FindStringSubmatch(file.Name()) 429 if len(match) == 0 { 430 continue 431 } 432 433 // If an OS keyword was used ensure it matches our runtime 434 l := strings.ToLower 435 keyword := l(match[1]) 436 if (keyword == l(sysinfo.Linux.String()) || keyword == l(sysinfo.Mac.String()) || keyword == l(sysinfo.Windows.String())) && 437 keyword != l(sysinfo.OS().String()) { 438 continue 439 } 440 441 secondaryProject, err := parse(filepath.Join(projectDir, file.Name())) 442 if err != nil { 443 return nil, err 444 } 445 if err := mergo.Merge(secondaryProject, *project, mergo.WithAppendSlice); err != nil { 446 return nil, errs.Wrap(err, "Could not merge %s into your activestate.yaml", file.Name()) 447 } 448 secondaryProject.path = project.path // keep original project path, not secondary path 449 project = secondaryProject 450 } 451 452 if err = project.Init(); err != nil { 453 return nil, errs.Wrap(err, "project.Init failed") 454 } 455 456 cfg, err := config.New() 457 if err != nil { 458 return nil, errs.Wrap(err, "Could not read configuration required by projectfile parser.") 459 } 460 defer rtutils.Closer(cfg.Close, &rerr) 461 462 namespace := fmt.Sprintf("%s/%s", project.parsedURL.Owner, project.parsedURL.Name) 463 StoreProjectMapping(cfg, namespace, filepath.Dir(project.Path())) 464 465 // Migrate project file if needed 466 if !migrationRunning && project.ConfigVersion != ConfigVersion && migrator != nil { 467 // Migrations may themselves utilize the projectfile package, so we have to ensure we don't start an infinite loop 468 migrationRunning = true 469 defer func() { migrationRunning = false }() 470 471 if project.ConfigVersion > ConfigVersion { 472 return nil, locale.NewInputError("err_projectfile_version_too_high") 473 } 474 updatedConfigVersion, errMigrate := migrator(project, ConfigVersion) 475 476 // Ensure we update the config version regardless of any error that occurred, because we don't want to repeat 477 // the same version migrations 478 project.ConfigVersion = updatedConfigVersion 479 if err := NewYamlField("config_version", ConfigVersion).Save(project.Path()); err != nil { 480 return nil, errs.Pack(errMigrate, errs.Wrap(err, "Could not save config_version")) 481 } 482 483 if errMigrate != nil { 484 return nil, errs.Wrap(errMigrate, "Migrator failed") 485 } 486 } 487 488 return project, nil 489 } 490 491 // Init initializes the parsedURL field from the project url string 492 func (p *Project) Init() error { 493 parsedURL, err := p.parseURL() 494 if err != nil { 495 return locale.WrapInputError(err, "parse_project_file_url_err", "Could not parse project url: {{.V0}}.", p.Project) 496 } 497 p.parsedURL = parsedURL 498 499 // Ensure branch name is set 500 if p.parsedURL.Owner != "" && p.parsedURL.BranchName == "" { 501 logging.Debug("Appending default branch as none is set") 502 if err := p.SetBranch(constants.DefaultBranchName); err != nil { 503 return locale.WrapError(err, "err_set_default_branch", "", constants.DefaultBranchName) 504 } 505 } 506 507 if p.Lock != "" { 508 parsedLock, err := ParseLock(p.Lock) 509 if err != nil { 510 return errs.Wrap(err, "ParseLock %s failed", p.Lock) 511 } 512 513 p.parsedChannel = parsedLock.Channel 514 p.parsedVersion = parsedLock.Version 515 } 516 517 return nil 518 } 519 520 func parse(configFilepath string) (*Project, error) { 521 if !fileutils.FileExists(configFilepath) { 522 return nil, &ErrorNoProject{locale.NewInputError("err_no_projectfile")} 523 } 524 525 dat, err := os.ReadFile(configFilepath) 526 if err != nil { 527 return nil, errs.Wrap(err, "os.ReadFile %s failure", configFilepath) 528 } 529 530 return parseData(dat, configFilepath) 531 } 532 533 func parseData(dat []byte, configFilepath string) (*Project, error) { 534 if err := detectDeprecations(dat, configFilepath); err != nil { 535 return nil, errs.Wrap(err, "deprecations found") 536 } 537 538 project := Project{} 539 err2 := yaml.Unmarshal(dat, &project) 540 project.path = configFilepath 541 542 if err2 != nil { 543 return nil, &ErrorParseProject{locale.NewExternalError( 544 "err_project_parsed", 545 "Project file `{{.V1}}` could not be parsed, the parser produced the following error: {{.V0}}", err2.Error(), configFilepath), 546 } 547 } 548 549 return &project, nil 550 } 551 552 func detectDeprecations(dat []byte, configFilepath string) error { 553 deprecations := deprecatedRegex.FindAllIndex(dat, -1) 554 if len(deprecations) == 0 { 555 return nil 556 } 557 deplist := []string{} 558 for _, depIdxs := range deprecations { 559 dep := strings.TrimSpace(strings.TrimSuffix(string(dat[depIdxs[0]:depIdxs[1]]), ":")) 560 deplist = append(deplist, locale.Tr("pjfile_deprecation_entry", dep, strconv.Itoa(depIdxs[0]))) 561 } 562 return &ErrorParseProject{locale.NewExternalError( 563 "pjfile_deprecation_msg", 564 "", configFilepath, strings.Join(deplist, "\n"), constants.DocumentationURL+"config/#deprecation"), 565 } 566 } 567 568 // URL returns the project namespace's string URL from activestate.yaml. 569 func (p *Project) URL() string { 570 return p.Project 571 } 572 573 // Owner returns the project namespace's organization 574 func (p *Project) Owner() string { 575 return p.parsedURL.Owner 576 } 577 578 // Name returns the project namespace's name 579 func (p *Project) Name() string { 580 return p.parsedURL.Name 581 } 582 583 // BranchName returns the branch name specified in the project 584 func (p *Project) BranchName() string { 585 return p.parsedURL.BranchName 586 } 587 588 // Path returns the project's activestate.yaml file path. 589 func (p *Project) Path() string { 590 return p.path 591 } 592 593 // LegacyCommitID is for use by legacy mechanics ONLY 594 // It returns a pre-migrated project's commit ID from activestate.yaml. 595 func (p *Project) LegacyCommitID() string { 596 return p.parsedURL.LegacyCommitID 597 } 598 599 // SetLegacyCommit sets the commit id within the current project file. This is done 600 // in-place so that line order is preserved. 601 func (p *Project) SetLegacyCommit(commitID string) error { 602 pf := NewProjectField() 603 if err := pf.LoadProject(p.Project); err != nil { 604 return errs.Wrap(err, "Could not load activestate.yaml") 605 } 606 pf.SetLegacyCommitID(commitID) 607 if err := pf.Save(p.path); err != nil { 608 return errs.Wrap(err, "Could not save activestate.yaml") 609 } 610 611 p.parsedURL.LegacyCommitID = commitID 612 p.Project = pf.String() 613 return nil 614 } 615 616 func (p *Project) Dir() string { 617 return filepath.Dir(p.path) 618 } 619 620 // SetPath sets the path of the project file and should generally only be used by tests 621 func (p *Project) SetPath(path string) { 622 p.path = path 623 } 624 625 // Channel returns the channel as it was interpreted from the lock 626 func (p *Project) Channel() string { 627 return p.parsedChannel 628 } 629 630 // Version returns the version as it was interpreted from the lock 631 func (p *Project) Version() string { 632 return p.parsedVersion 633 } 634 635 // ValidateProjectURL validates the configured project URL 636 func ValidateProjectURL(url string) error { 637 // Note: This line also matches headless commit URLs: match == {'commit', '<commit_id>'} 638 match := ProjectURLRe.FindStringSubmatch(url) 639 if len(match) < 3 { 640 return &ErrorParseProject{locale.NewError("err_bad_project_url")} 641 } 642 return nil 643 } 644 645 // Reload the project file from disk 646 func (p *Project) Reload() error { 647 pj, err := Parse(p.path) 648 if err != nil { 649 return err 650 } 651 *p = *pj 652 return nil 653 } 654 655 // Save the project to its activestate.yaml file 656 func (p *Project) Save(cfg ConfigGetter) error { 657 return p.save(cfg, p.Path()) 658 } 659 660 // parseURL returns the parsed fields of a Project URL 661 func (p *Project) parseURL() (projectURL, error) { 662 return parseURL(p.Project) 663 } 664 665 func validateUUID(uuidStr string) error { 666 if ok := strfmt.Default.Validates("uuid", uuidStr); !ok { 667 return locale.NewError("err_commit_id_invalid", "", uuidStr) 668 } 669 670 var uuid strfmt.UUID 671 if err := uuid.UnmarshalText([]byte(uuidStr)); err != nil { 672 return locale.WrapError(err, "err_commit_id_unmarshal", "Failed to unmarshal the commit id {{.V0}} read from activestate.yaml.", uuidStr) 673 } 674 675 return nil 676 } 677 678 func parseURL(rawURL string) (projectURL, error) { 679 p := projectURL{} 680 681 err := ValidateProjectURL(rawURL) 682 if err != nil { 683 return p, err 684 } 685 686 u, err := url.Parse(rawURL) 687 if err != nil { 688 return p, errs.Wrap(err, "Could not parse URL") 689 } 690 691 path := strings.Split(u.Path, "/") 692 if len(path) > 2 { 693 if path[1] == "commit" { 694 p.LegacyCommitID = path[2] 695 } else { 696 p.Owner = path[1] 697 p.Name = path[2] 698 } 699 } 700 701 q := u.Query() 702 if c := q.Get("commitID"); c != "" { 703 p.LegacyCommitID = c 704 } 705 706 if p.LegacyCommitID != "" { 707 if err := validateUUID(p.LegacyCommitID); err != nil { 708 return p, err 709 } 710 } 711 712 if b := q.Get("branch"); b != "" { 713 p.BranchName = b 714 } 715 716 return p, nil 717 } 718 719 // Save the project to its activestate.yaml file 720 func (p *Project) save(cfg ConfigGetter, path string) error { 721 dat, err := yaml.Marshal(p) 722 if err != nil { 723 return errs.Wrap(err, "yaml.Marshal failed") 724 } 725 726 err = ValidateProjectURL(p.Project) 727 if err != nil { 728 return errs.Wrap(err, "ValidateProjectURL failed") 729 } 730 731 logging.Debug("Saving %s", path) 732 733 f, err := os.Create(path) 734 if err != nil { 735 return errs.Wrap(err, "os.Create %s failed", path) 736 } 737 defer f.Close() 738 739 _, err = f.Write([]byte(dat)) 740 if err != nil { 741 return errs.Wrap(err, "f.Write %s failed", path) 742 } 743 744 if cfg != nil { 745 StoreProjectMapping(cfg, fmt.Sprintf("%s/%s", p.parsedURL.Owner, p.parsedURL.Name), filepath.Dir(p.Path())) 746 } 747 748 return nil 749 } 750 751 // SetNamespace updates the namespace in the project file 752 func (p *Project) SetNamespace(owner, project string) error { 753 pf := NewProjectField() 754 if err := pf.LoadProject(p.Project); err != nil { 755 return errs.Wrap(err, "Could not load activestate.yaml") 756 } 757 pf.SetNamespace(owner, project) 758 if err := pf.Save(p.path); err != nil { 759 return errs.Wrap(err, "Could not save activestate.yaml") 760 } 761 762 // keep parsed url components in sync 763 p.parsedURL.Owner = owner 764 p.parsedURL.Name = project 765 p.Project = pf.String() 766 767 return nil 768 } 769 770 // SetBranch sets the branch within the current project file. This is done 771 // in-place so that line order is preserved. 772 func (p *Project) SetBranch(branch string) error { 773 pf := NewProjectField() 774 775 if err := pf.LoadProject(p.Project); err != nil { 776 return errs.Wrap(err, "Could not load activestate.yaml") 777 } 778 779 pf.SetBranch(branch) 780 781 if !condition.InUnitTest() || p.path != "" { 782 if err := pf.Save(p.path); err != nil { 783 return errs.Wrap(err, "Could not save activestate.yaml") 784 } 785 } 786 787 p.parsedURL.BranchName = branch 788 p.Project = pf.String() 789 return nil 790 } 791 792 // GetProjectFilePath returns the path to the project activestate.yaml 793 // It considers projects in the following order: 794 // 1. Environment variable (e.g. `state shell` sets one) 795 // 2. Working directory (i.e. walk up directory tree looking for activestate.yaml) 796 // 3. Fall back on default project 797 func GetProjectFilePath() (string, error) { 798 defer profile.Measure("GetProjectFilePath", time.Now()) 799 lookup := []func() (string, error){ 800 getProjectFilePathFromEnv, 801 getProjectFilePathFromWd, 802 getProjectFilePathFromDefault, 803 } 804 for _, getProjectFilePath := range lookup { 805 path, err := getProjectFilePath() 806 if err != nil { 807 return "", errs.Wrap(err, "getProjectFilePath failed") 808 } 809 if path != "" { 810 return path, nil 811 } 812 } 813 814 return "", &ErrorNoProject{locale.NewInputError("err_no_projectfile")} 815 } 816 817 func getProjectFilePathFromEnv() (string, error) { 818 var projectFilePath string 819 820 if activatedProjectDirPath := os.Getenv(constants.ActivatedStateEnvVarName); activatedProjectDirPath != "" { 821 projectFilePath = filepath.Join(activatedProjectDirPath, constants.ConfigFileName) 822 } else { 823 projectFilePath = os.Getenv(constants.ProjectEnvVarName) 824 } 825 826 if projectFilePath != "" { 827 if fileutils.FileExists(projectFilePath) { 828 return projectFilePath, nil 829 } 830 return "", &ErrorNoProjectFromEnv{locale.NewInputError("err_project_env_file_not_exist", "", projectFilePath)} 831 } 832 833 return "", nil 834 } 835 836 func getProjectFilePathFromWd() (string, error) { 837 root, err := osutils.Getwd() 838 if err != nil { 839 return "", errs.Wrap(err, "osutils.Getwd failed") 840 } 841 842 path, err := fileutils.FindFileInPath(root, constants.ConfigFileName) 843 if err != nil && !errors.Is(err, fileutils.ErrorFileNotFound) { 844 return "", errs.Wrap(err, "fileutils.FindFileInPath %s failed", root) 845 } 846 847 return path, nil 848 } 849 850 func getProjectFilePathFromDefault() (_ string, rerr error) { 851 cfg, err := config.New() 852 if err != nil { 853 return "", errs.Wrap(err, "Could not read configuration required to determine which project to use") 854 } 855 defer rtutils.Closer(cfg.Close, &rerr) 856 857 defaultProjectPath := cfg.GetString(constants.GlobalDefaultPrefname) 858 if defaultProjectPath == "" { 859 return "", nil 860 } 861 862 path, err := fileutils.FindFileInPath(defaultProjectPath, constants.ConfigFileName) 863 if err != nil { 864 if !errors.Is(err, fileutils.ErrorFileNotFound) { 865 return "", errs.Wrap(err, "fileutils.FindFileInPath %s failed", defaultProjectPath) 866 } 867 return "", &ErrorNoDefaultProject{locale.NewInputError("err_no_default_project", "Could not find your project at: [ACTIONABLE]{{.V0}}[/RESET]", defaultProjectPath)} 868 } 869 return path, nil 870 } 871 872 // GetPersisted gets the persisted project, if any 873 func GetPersisted() *Project { 874 return persistentProject 875 } 876 877 func Get() (*Project, error) { 878 if persistentProject != nil { 879 return persistentProject, nil 880 } 881 882 project, err := GetOnce() 883 if err != nil { 884 return nil, err 885 } 886 887 err = project.Persist() 888 if err != nil { 889 return nil, err 890 } 891 return project, nil 892 } 893 894 // GetOnce returns the project configuration in a safe manner (returns error), the same as GetSafe, but it avoids persisting the project 895 func GetOnce() (*Project, error) { 896 // we do not want to use a path provided by state if we're running tests 897 projectFilePath, err := GetProjectFilePath() 898 if err != nil { 899 if errors.Is(err, fileutils.ErrorFileNotFound) { 900 return nil, &ErrorNoProject{locale.WrapError(err, "err_project_file_notfound", "Could not detect project file path.")} 901 } 902 return nil, err 903 } 904 905 project, err := Parse(projectFilePath) 906 if err != nil { 907 return nil, errs.Wrap(err, "Could not parse projectfile") 908 } 909 910 return project, nil 911 } 912 913 // FromPath will return the projectfile that's located at the given path (this will walk up the directory tree until it finds the project) 914 func FromPath(path string) (*Project, error) { 915 defer profile.Measure("projectfile:FromPath", time.Now()) 916 // we do not want to use a path provided by state if we're running tests 917 projectFilePath, err := fileutils.FindFileInPath(path, constants.ConfigFileName) 918 if err != nil { 919 return nil, &ErrorNoProject{locale.WrapInputError(err, "err_project_not_found", "", path)} 920 } 921 922 _, err = os.ReadFile(projectFilePath) 923 if err != nil { 924 logging.Warning("Cannot load config file: %v", err) 925 return nil, &ErrorNoProject{locale.WrapInputError(err, "err_no_projectfile")} 926 } 927 project, err := Parse(projectFilePath) 928 if err != nil { 929 return nil, errs.Wrap(err, "Could not parse projectfile") 930 } 931 932 return project, nil 933 } 934 935 // FromExactPath will return the projectfile that's located at the given path without walking up the directory tree 936 func FromExactPath(path string) (*Project, error) { 937 // we do not want to use a path provided by state if we're running tests 938 projectFilePath := filepath.Join(path, constants.ConfigFileName) 939 940 if !fileutils.FileExists(projectFilePath) { 941 return nil, &ErrorNoProject{locale.NewInputError("err_no_projectfile")} 942 } 943 944 _, err := os.ReadFile(projectFilePath) 945 if err != nil { 946 logging.Warning("Cannot load config file: %v", err) 947 return nil, &ErrorNoProject{locale.WrapInputError(err, "err_no_projectfile")} 948 } 949 project, err := Parse(projectFilePath) 950 if err != nil { 951 return nil, errs.Wrap(err, "Could not parse projectfile") 952 } 953 954 return project, nil 955 } 956 957 // CreateParams are parameters that we create a custom activestate.yaml file from 958 type CreateParams struct { 959 Owner string 960 Project string 961 BranchName string 962 Directory string 963 Content string 964 Language string 965 Private bool 966 path string 967 ProjectURL string 968 Cache string 969 } 970 971 // Create will create a new activestate.yaml with a projectURL for the given details 972 func Create(params *CreateParams) (*Project, error) { 973 lang := language.MakeByName(params.Language) 974 err := validateCreateParams(params) 975 if err != nil { 976 return nil, err 977 } 978 979 return createCustom(params, lang) 980 } 981 982 func createCustom(params *CreateParams, lang language.Language) (*Project, error) { 983 err := fileutils.MkdirUnlessExists(params.Directory) 984 if err != nil { 985 return nil, err 986 } 987 988 if params.ProjectURL == "" { 989 // Note: cannot use api.GetPlatformURL() due to import cycle. 990 host := constants.DefaultAPIHost 991 if hostOverride := os.Getenv(constants.APIHostEnvVarName); hostOverride != "" { 992 host = hostOverride 993 } 994 u, err := url.Parse(fmt.Sprintf("https://%s/%s/%s", host, params.Owner, params.Project)) 995 if err != nil { 996 return nil, errs.Wrap(err, "url parse new project url failed") 997 } 998 q := u.Query() 999 1000 if params.BranchName != "" { 1001 q.Set("branch", params.BranchName) 1002 } 1003 1004 u.RawQuery = q.Encode() 1005 params.ProjectURL = u.String() 1006 } 1007 1008 params.path = filepath.Join(params.Directory, constants.ConfigFileName) 1009 if fileutils.FileExists(params.path) { 1010 return nil, locale.NewInputError("err_projectfile_exists") 1011 } 1012 1013 err = ValidateProjectURL(params.ProjectURL) 1014 if err != nil { 1015 return nil, err 1016 } 1017 match := ProjectURLRe.FindStringSubmatch(params.ProjectURL) 1018 if len(match) < 3 { 1019 return nil, locale.NewInputError("err_projectfile_invalid_url") 1020 } 1021 owner, project := match[1], match[2] 1022 1023 shell := "bash" 1024 if runtime.GOOS == "windows" { 1025 shell = "batch" 1026 } 1027 1028 languageDisabled := os.Getenv(constants.DisableLanguageTemplates) == "true" 1029 content := params.Content 1030 if !languageDisabled && content == "" && lang != language.Unset && lang != language.Unknown { 1031 tplName := "activestate.yaml." + strings.TrimRight(lang.String(), "23") + ".tpl" 1032 template, err := assets.ReadFileBytes(tplName) 1033 if err != nil { 1034 return nil, errs.Wrap(err, "Could not read asset") 1035 } 1036 content, err = strutils.ParseTemplate( 1037 string(template), 1038 map[string]interface{}{"Owner": owner, "Project": project, "Shell": shell, "Language": lang.String(), "LangExe": lang.Executable().Filename()}, 1039 nil) 1040 if err != nil { 1041 return nil, errs.Wrap(err, "Could not parse %s", tplName) 1042 } 1043 } 1044 1045 data := map[string]interface{}{ 1046 "Project": params.ProjectURL, 1047 "Content": content, 1048 "Private": params.Private, 1049 "ConfigVersion": ConfigVersion, 1050 } 1051 1052 tplName := "activestate.yaml.tpl" 1053 tplContents, err := assets.ReadFileBytes(tplName) 1054 if err != nil { 1055 return nil, errs.Wrap(err, "Could not read asset") 1056 } 1057 fileContents, err := strutils.ParseTemplate(string(tplContents), data, nil) 1058 if err != nil { 1059 return nil, errs.Wrap(err, "Could not parse %s", tplName) 1060 } 1061 1062 err = fileutils.WriteFile(params.path, []byte(fileContents)) 1063 if err != nil { 1064 return nil, err 1065 } 1066 1067 if params.Cache != "" { 1068 createErr := createHostFile(params.Directory, params.Cache) 1069 if createErr != nil { 1070 return nil, errs.Wrap(createErr, "Could not create cache file") 1071 } 1072 } 1073 1074 return Parse(params.path) 1075 } 1076 1077 func createHostFile(filePath, cachePath string) error { 1078 user, err := user.Current() 1079 if err != nil { 1080 return errs.Wrap(err, "Could not get current user") 1081 } 1082 1083 data := map[string]interface{}{ 1084 "Cache": cachePath, 1085 } 1086 1087 tplName := "activestate.yaml.cache.tpl" 1088 tplContents, err := assets.ReadFileBytes(tplName) 1089 if err != nil { 1090 return errs.Wrap(err, "Could not read asset") 1091 } 1092 1093 fileContents, err := strutils.ParseTemplate(string(tplContents), data, nil) 1094 if err != nil { 1095 return errs.Wrap(err, "Could not parse %s", tplName) 1096 } 1097 1098 // Trim any non-alphanumeric characters from the username 1099 if err := fileutils.WriteFile(filepath.Join(filePath, fmt.Sprintf("activestate.%s.yaml", nonAlphanumericRegex.ReplaceAllString(user.Username, ""))), []byte(fileContents)); err != nil { 1100 return errs.Wrap(err, "Could not write cache file") 1101 } 1102 1103 return nil 1104 } 1105 1106 func validateCreateParams(params *CreateParams) error { 1107 switch { 1108 case params.Directory == "": 1109 return locale.NewInputError("err_project_require_path") 1110 case params.ProjectURL != "": 1111 return nil // Owner and Project not required when projectURL is set 1112 case params.Owner == "": 1113 return locale.NewInputError("err_project_require_owner") 1114 case params.Project == "": 1115 return locale.NewInputError("err_project_require_name") 1116 default: 1117 return nil 1118 } 1119 } 1120 1121 // ParseVersionInfo parses the lock field from the projectfile and updates 1122 // the activestate.yaml if an older version representation is present 1123 func ParseVersionInfo(projectFilePath string) (*VersionInfo, error) { 1124 if !fileutils.FileExists(projectFilePath) { 1125 return nil, nil 1126 } 1127 1128 dat, err := os.ReadFile(projectFilePath) 1129 if err != nil { 1130 return nil, errs.Wrap(err, "os.ReadFile %s failed", projectFilePath) 1131 } 1132 1133 versionStruct := VersionInfo{} 1134 err = yaml.Unmarshal(dat, &versionStruct) 1135 if err != nil { 1136 return nil, &ErrorParseProject{locale.WrapError(err, "Could not unmarshal activestate.yaml")} 1137 } 1138 1139 if versionStruct.Lock == "" { 1140 return nil, nil 1141 } 1142 1143 return ParseLock(versionStruct.Lock) 1144 } 1145 1146 func ParseLock(lock string) (*VersionInfo, error) { 1147 split := strings.Split(lock, "@") 1148 if len(split) != 2 { 1149 return nil, locale.NewInputError("err_invalid_lock", "", lock) 1150 } 1151 1152 return &VersionInfo{ 1153 Channel: split[0], 1154 Version: split[1], 1155 Lock: lock, 1156 }, nil 1157 } 1158 1159 // AddLockInfo adds the lock field to activestate.yaml 1160 func AddLockInfo(projectFilePath, branch, version string) error { 1161 data, err := cleanVersionInfo(projectFilePath) 1162 if err != nil { 1163 return locale.WrapError(err, "err_clean_projectfile", "Could not remove old version information from projectfile", projectFilePath) 1164 } 1165 1166 lockRegex := regexp.MustCompile(`(?m)^lock:.*`) 1167 if lockRegex.Match(data) { 1168 versionUpdate := []byte(fmt.Sprintf("lock: %s@%s", branch, version)) 1169 replaced := lockRegex.ReplaceAll(data, versionUpdate) 1170 return os.WriteFile(projectFilePath, replaced, 0644) 1171 } 1172 1173 projectRegex := regexp.MustCompile(fmt.Sprintf("(?m:(^project:\\s*%s))", ProjectURLRe)) 1174 lockString := fmt.Sprintf("%s@%s", branch, version) 1175 lockUpdate := []byte(fmt.Sprintf("${1}\nlock: %s", lockString)) 1176 1177 data, err = os.ReadFile(projectFilePath) 1178 if err != nil { 1179 return err 1180 } 1181 1182 updated := projectRegex.ReplaceAll(data, lockUpdate) 1183 1184 return os.WriteFile(projectFilePath, updated, 0644) 1185 } 1186 1187 func RemoveLockInfo(projectFilePath string) error { 1188 data, err := os.ReadFile(projectFilePath) 1189 if err != nil { 1190 return locale.WrapError(err, "err_read_projectfile", "", projectFilePath) 1191 } 1192 1193 lockRegex := regexp.MustCompile(`(?m)^lock:.*`) 1194 clean := lockRegex.ReplaceAll(data, []byte("")) 1195 1196 err = os.WriteFile(projectFilePath, clean, 0644) 1197 if err != nil { 1198 return locale.WrapError(err, "err_write_unlocked_projectfile", "Could not remove lock from projectfile") 1199 } 1200 1201 return nil 1202 } 1203 1204 func cleanVersionInfo(projectFilePath string) ([]byte, error) { 1205 data, err := os.ReadFile(projectFilePath) 1206 if err != nil { 1207 return nil, locale.WrapError(err, "err_read_projectfile", "", projectFilePath) 1208 } 1209 1210 branchRegex := regexp.MustCompile(`(?m:^branch:\s*\w+\n)`) 1211 clean := branchRegex.ReplaceAll(data, []byte("")) 1212 1213 versionRegex := regexp.MustCompile(`(?m:^version:\s*\d+.\d+.\d+-[A-Za-z0-9]+\n)`) 1214 clean = versionRegex.ReplaceAll(clean, []byte("")) 1215 1216 err = os.WriteFile(projectFilePath, clean, 0644) 1217 if err != nil { 1218 return nil, locale.WrapError(err, "err_write_clean_projectfile", "Could not write cleaned projectfile information") 1219 } 1220 1221 return clean, nil 1222 } 1223 1224 // Reset the current state, which unsets the persistent project 1225 func Reset() { 1226 persistentProject = nil 1227 os.Unsetenv(constants.ProjectEnvVarName) 1228 } 1229 1230 // Persist "activates" the given project and makes it such that subsequent calls 1231 // to Get() return this project. 1232 // Only one project can persist at a time. 1233 func (p *Project) Persist() error { 1234 if p.Project == "" { 1235 return locale.NewError(locale.T("err_invalid_project")) 1236 } 1237 persistentProject = p 1238 os.Setenv(constants.ProjectEnvVarName, p.Path()) 1239 return nil 1240 } 1241 1242 type ConfigGetter interface { 1243 GetStringMapStringSlice(key string) map[string][]string 1244 AllKeys() []string 1245 GetStringSlice(string) []string 1246 GetString(string) string 1247 Set(string, interface{}) error 1248 GetThenSet(string, func(interface{}) (interface{}, error)) error 1249 Close() error 1250 } 1251 1252 func GetProjectMapping(config ConfigGetter) map[string][]string { 1253 addDeprecatedProjectMappings(config) 1254 CleanProjectMapping(config) 1255 projects := config.GetStringMapStringSlice(LocalProjectsConfigKey) 1256 if projects == nil { 1257 return map[string][]string{} 1258 } 1259 return projects 1260 } 1261 1262 // GetStaleProjectMapping returns a project mapping from the last time the 1263 // state tool was run. This mapping could include projects that are no longer 1264 // on the system. 1265 func GetStaleProjectMapping(config ConfigGetter) map[string][]string { 1266 addDeprecatedProjectMappings(config) 1267 projects := config.GetStringMapStringSlice(LocalProjectsConfigKey) 1268 if projects == nil { 1269 return map[string][]string{} 1270 } 1271 return projects 1272 } 1273 1274 func GetProjectFileMapping(config ConfigGetter) map[string][]*Project { 1275 projects := GetProjectMapping(config) 1276 1277 res := make(map[string][]*Project) 1278 for name, paths := range projects { 1279 if name == "/" { 1280 continue 1281 } 1282 var pFiles []*Project 1283 for _, path := range paths { 1284 prj, err := FromExactPath(path) 1285 if err != nil { 1286 multilog.Error("Could not read project file at %s: %v", path, err) 1287 continue 1288 } 1289 pFiles = append(pFiles, prj) 1290 } 1291 if len(pFiles) > 0 { 1292 res[name] = pFiles 1293 } 1294 } 1295 return res 1296 } 1297 1298 func GetCachedProjectNameForPath(config ConfigGetter, projectPath string) string { 1299 projects := GetProjectMapping(config) 1300 1301 for name, paths := range projects { 1302 if name == "/" { 1303 continue 1304 } 1305 for _, path := range paths { 1306 if isEqual, err := fileutils.PathsEqual(projectPath, path); isEqual { 1307 if err != nil { 1308 logging.Debug("Failed to compare paths %s and %s", projectPath, path) 1309 } 1310 return name 1311 } 1312 } 1313 } 1314 return "" 1315 } 1316 1317 func addDeprecatedProjectMappings(cfg ConfigGetter) { 1318 var unsets []string 1319 1320 err := cfg.GetThenSet( 1321 LocalProjectsConfigKey, 1322 func(v interface{}) (interface{}, error) { 1323 projects, err := cast.ToStringMapStringSliceE(v) 1324 if err != nil && v != nil { // don't report if error due to nil input 1325 multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Projects data in config is abnormal (type: %T)", v) 1326 } 1327 1328 keys := funk.FilterString(cfg.AllKeys(), func(v string) bool { 1329 return strings.HasPrefix(v, "project_") 1330 }) 1331 1332 for _, key := range keys { 1333 namespace := strings.TrimPrefix(key, "project_") 1334 newPaths := projects[namespace] 1335 paths := cfg.GetStringSlice(key) 1336 projects[namespace] = funk.UniqString(append(newPaths, paths...)) 1337 unsets = append(unsets, key) 1338 } 1339 1340 return projects, nil 1341 }, 1342 ) 1343 if err != nil { 1344 multilog.Error("Could not update project mapping in config, error: %v", err) 1345 } 1346 for _, unset := range unsets { 1347 if err := cfg.Set(unset, nil); err != nil { 1348 multilog.Error("Could not clear config entry for key %s, error: %v", unset, err) 1349 } 1350 } 1351 1352 } 1353 1354 // GetProjectPaths returns the paths of all projects associated with the namespace 1355 func GetProjectPaths(cfg ConfigGetter, namespace string) []string { 1356 projects := GetProjectMapping(cfg) 1357 1358 // match case-insensitively 1359 var paths []string 1360 for key, value := range projects { 1361 if strings.EqualFold(key, namespace) { 1362 paths = append(paths, value...) 1363 } 1364 } 1365 1366 return paths 1367 } 1368 1369 // StoreProjectMapping associates the namespace with the project 1370 // path in the config 1371 func StoreProjectMapping(cfg ConfigGetter, namespace, projectPath string) { 1372 SetRecentlyUsedNamespace(cfg, namespace) 1373 err := cfg.GetThenSet( 1374 LocalProjectsConfigKey, 1375 func(v interface{}) (interface{}, error) { 1376 projects, err := cast.ToStringMapStringSliceE(v) 1377 if err != nil && v != nil { // don't report if error due to nil input 1378 multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Projects data in config is abnormal (type: %T)", v) 1379 } 1380 1381 projectPath, err = fileutils.ResolveUniquePath(projectPath) 1382 if err != nil { 1383 multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Could not resolve uniqe project path, %v", err) 1384 projectPath = filepath.Clean(projectPath) 1385 } 1386 1387 for name, paths := range projects { 1388 for i, path := range paths { 1389 path, err = fileutils.ResolveUniquePath(path) 1390 if err != nil { 1391 multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Could not resolve unique path, :%v", err) 1392 path = filepath.Clean(path) 1393 } 1394 1395 if path == projectPath { 1396 projects[name] = sliceutils.RemoveFromStrings(projects[name], i) 1397 } 1398 1399 if len(projects[name]) == 0 { 1400 delete(projects, name) 1401 } 1402 } 1403 } 1404 1405 paths := projects[namespace] 1406 if paths == nil { 1407 paths = make([]string, 0) 1408 } 1409 1410 if !funk.Contains(paths, projectPath) { 1411 paths = append(paths, projectPath) 1412 } 1413 1414 projects[namespace] = paths 1415 1416 return projects, nil 1417 }, 1418 ) 1419 if err != nil { 1420 multilog.Error("Could not set project mapping in config, error: %v", errs.JoinMessage(err)) 1421 } 1422 } 1423 1424 // CleanProjectMapping removes projects that no longer exist 1425 // on a user's filesystem from the projects config entry 1426 func CleanProjectMapping(cfg ConfigGetter) { 1427 err := cfg.GetThenSet( 1428 LocalProjectsConfigKey, 1429 func(v interface{}) (interface{}, error) { 1430 projects, err := cast.ToStringMapStringSliceE(v) 1431 if err != nil && v != nil { // don't report if error due to nil input 1432 multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Projects data in config is abnormal (type: %T)", v) 1433 } 1434 1435 seen := make(map[string]struct{}) 1436 1437 for namespace, paths := range projects { 1438 var removals []int 1439 for i, path := range paths { 1440 configFile := filepath.Join(path, constants.ConfigFileName) 1441 if !fileutils.DirExists(path) || !fileutils.FileExists(configFile) { 1442 removals = append(removals, i) 1443 continue 1444 } 1445 // Only remove the project if the activestate.yaml is parseable and there is a namespace 1446 // mismatch. 1447 // (We do not want to punish anyone for a syntax error when manually editing the file.) 1448 if proj, err := parse(configFile); err == nil && proj.Init() == nil { 1449 projNamespace := fmt.Sprintf("%s/%s", proj.Owner(), proj.Name()) 1450 if namespace != projNamespace { 1451 removals = append(removals, i) 1452 } 1453 } 1454 } 1455 1456 projects[namespace] = sliceutils.RemoveFromStrings(projects[namespace], removals...) 1457 if _, ok := seen[strings.ToLower(namespace)]; ok || len(projects[namespace]) == 0 { 1458 delete(projects, namespace) 1459 continue 1460 } 1461 seen[strings.ToLower(namespace)] = struct{}{} 1462 } 1463 1464 return projects, nil 1465 }, 1466 ) 1467 if err != nil { 1468 logging.Debug("Could not clean project mapping in config, error: %v", err) 1469 } 1470 } 1471 1472 func SetRecentlyUsedNamespace(cfg ConfigGetter, namespace string) { 1473 err := cfg.Set(constants.LastUsedNamespacePrefname, namespace) 1474 if err != nil { 1475 logging.Debug("Could not set recently used namespace in config, error: %v", err) 1476 } 1477 }