diff --git a/cmd/state-installer/cmd.go b/cmd/state-installer/cmd.go index 0fe66953c9..f56101ff30 100644 --- a/cmd/state-installer/cmd.go +++ b/cmd/state-installer/cmd.go @@ -133,7 +133,7 @@ func main() { "state-installer", "", "Installs or updates the State Tool", - primer.New(nil, out, nil, nil, nil, nil, cfg, nil, nil, an), + primer.New(nil, out, nil, nil, nil, cfg, nil, nil, an), []*captain.Flag{ // The naming of these flags is slightly inconsistent due to backwards compatibility requirements { Name: "command", diff --git a/cmd/state-remote-installer/main.go b/cmd/state-remote-installer/main.go index bbbeee32c8..09166caf4e 100644 --- a/cmd/state-remote-installer/main.go +++ b/cmd/state-remote-installer/main.go @@ -104,7 +104,7 @@ func main() { "state-installer", "", "Installs or updates the State Tool", - primer.New(nil, out, nil, nil, nil, nil, cfg, nil, nil, an), + primer.New(nil, out, nil, nil, nil, cfg, nil, nil, an), []*captain.Flag{ // The naming of these flags is slightly inconsistent due to backwards compatibility requirements { Name: "channel", diff --git a/cmd/state-svc/main.go b/cmd/state-svc/main.go index 69e0b84568..8c7228d03b 100644 --- a/cmd/state-svc/main.go +++ b/cmd/state-svc/main.go @@ -109,7 +109,7 @@ func run(cfg *config.Instance) error { return runStart(out, "svc-start:mouse") } - p := primer.New(nil, out, nil, nil, nil, nil, cfg, nil, nil, an) + p := primer.New(nil, out, nil, nil, nil, cfg, nil, nil, an) cmd := captain.NewCommand( path.Base(os.Args[0]), "", "", p, nil, nil, diff --git a/cmd/state/main.go b/cmd/state/main.go index eab10f4c07..833f42f5e6 100644 --- a/cmd/state/main.go +++ b/cmd/state/main.go @@ -11,13 +11,11 @@ import ( "strings" "time" - "github.com/ActiveState/cli/internal/captain" - "github.com/ActiveState/cli/cmd/state/internal/cmdtree" anAsync "github.com/ActiveState/cli/internal/analytics/client/async" + "github.com/ActiveState/cli/internal/captain" "github.com/ActiveState/cli/internal/config" "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/constraints" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/events" "github.com/ActiveState/cli/internal/installation" @@ -39,7 +37,7 @@ import ( "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" - "github.com/ActiveState/cli/pkg/projectfile" + "github.com/ActiveState/cli/pkg/projget" ) func main() { @@ -171,24 +169,14 @@ func run(args []string, isInteractive bool, cfg *config.Instance, out output.Out return logData }) - // Retrieve project file - pjPath, err := projectfile.GetProjectFilePath() - if err != nil && errs.Matches(err, &projectfile.ErrorNoProjectFromEnv{}) { - // Fail if we are meant to inherit the projectfile from the environment, but the file doesn't exist - return err - } + auth := authentication.New(cfg) + defer events.Close("auth", auth.Close) - // Set up project (if we have a valid path) - var pj *project.Project - if pjPath != "" { - pjf, err := projectfile.FromPath(pjPath) - if err != nil { - return err - } - pj, err = project.New(pjf, out) - if err != nil { - return err - } + sshell := subshell.New(cfg) + + pj, err := projget.NewProject(out, auth, sshell.Shell()) + if err != nil { + return err } pjNamespace := "" @@ -196,9 +184,6 @@ func run(args []string, isInteractive bool, cfg *config.Instance, out output.Out pjNamespace = pj.Namespace().String() } - auth := authentication.New(cfg) - defer events.Close("auth", auth.Close) - if err := auth.Sync(); err != nil { logging.Warning("Could not sync authenticated state: %s", err.Error()) } @@ -213,16 +198,10 @@ func run(args []string, isInteractive bool, cfg *config.Instance, out output.Out // Set up prompter prompter := prompt.New(isInteractive, an) - // Set up conditional, which accesses a lot of primer data - sshell := subshell.New(cfg) - - conditional := constraints.NewPrimeConditional(auth, pj, sshell.Shell()) - project.RegisterConditional(conditional) - project.RegisterExpander("mixin", project.NewMixin(auth).Expander) project.RegisterExpander("secrets", project.NewSecretPromptingExpander(secretsapi.Get(), prompter, cfg, auth)) // Run the actual command - cmds := cmdtree.New(primer.New(pj, out, auth, prompter, sshell, conditional, cfg, ipcClient, svcmodel, an), args...) + cmds := cmdtree.New(primer.New(pj, out, auth, prompter, sshell, cfg, ipcClient, svcmodel, an), args...) childCmd, err := cmds.Command().Find(args[1:]) if err != nil { diff --git a/internal/constraints/constraints.go b/internal/constraints/constraints.go index 0d12f7bcac..47d379c01e 100644 --- a/internal/constraints/constraints.go +++ b/internal/constraints/constraints.go @@ -3,7 +3,7 @@ package constraints import ( "bytes" "fmt" - "path/filepath" + "reflect" "regexp" "sort" "strings" @@ -12,27 +12,10 @@ import ( "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/logging" "github.com/ActiveState/cli/internal/multilog" - "github.com/ActiveState/cli/internal/rtutils/p" - "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/projectfile" - "github.com/ActiveState/cli/pkg/sysinfo" "github.com/thoas/go-funk" ) -var cache = make(map[string]interface{}) - -func getCache(key string, getter func() (interface{}, error)) (interface{}, error) { - if v, ok := cache[key]; ok { - return v, nil - } - v, err := getter() - if err != nil { - return nil, err - } - cache[key] = v - return v, err -} - // For testing. var osOverride, osVersionOverride, archOverride, libcOverride, compilerOverride string @@ -41,22 +24,9 @@ type Conditional struct { funcs template.FuncMap } -func NewConditional(a *authentication.Auth) *Conditional { +func NewConditional() *Conditional { c := &Conditional{map[string]interface{}{}, map[string]interface{}{}} - c.RegisterFunc("Mixin", func() map[string]interface{} { - res := map[string]string{ - "Name": "", - "Email": "", - } - if a.Authenticated() { - res["Name"] = a.WhoAmI() - res["Email"] = a.Email() - } - return map[string]interface{}{ - "User": res, - } - }) c.RegisterFunc("Contains", funk.Contains) c.RegisterFunc("HasPrefix", strings.HasPrefix) c.RegisterFunc("HasSuffix", strings.HasSuffix) @@ -72,62 +42,36 @@ func NewConditional(a *authentication.Auth) *Conditional { return c } -type projectable interface { - Owner() string - Name() string - NamespaceString() string - CommitID() string - BranchName() string - Path() string - URL() string -} +func NewPrimeConditional(structure interface{}) *Conditional { + c := NewConditional() -func NewPrimeConditional(auth *authentication.Auth, pj projectable, subshellName string) *Conditional { - var ( - pjOwner string - pjName string - pjNamespace string - pjURL string - pjCommit string - pjBranch string - pjPath string - ) - if !p.IsNil(pj) { - pjOwner = pj.Owner() - pjName = pj.Name() - pjNamespace = pj.NamespaceString() - pjURL = pj.URL() - pjCommit = pj.CommitID() - pjBranch = pj.BranchName() - pjPath = pj.Path() - if pjPath != "" { - pjPath = filepath.Dir(pjPath) - } + v := reflect.ValueOf(structure) + // deref if needed + if v.Kind() == reflect.Ptr { + v = v.Elem() } - c := NewConditional(auth) - c.RegisterParam("Project", map[string]string{ - "Owner": pjOwner, - "Name": pjName, - "Namespace": pjNamespace, - "Url": pjURL, - "Commit": pjCommit, - "Branch": pjBranch, - "Path": pjPath, - - // Legacy - "NamespacePrefix": pjNamespace, - }) - osVersion, err := sysinfo.OSVersion() - if err != nil { - multilog.Error("Could not detect OSVersion: %v", err) + fields := reflect.VisibleFields(v.Type()) + + // Work at depth 1: Vars.[Struct].Struct.Simple + for _, f := range fields { + d1Val := v.FieldByIndex(f.Index) + if d1Val.Kind() == reflect.Ptr { + d1Val = d1Val.Elem() + } + + // Only nodes at depth 1 need to be registered since the generic type + // handling within the templating package will do the rest. If function + // registration is needed at greater depths, this will need to be + // reworked (and may not be possible without expansive refactoring). + switch d1Val.Type().Kind() { + case reflect.Func: + c.RegisterFunc(f.Name, d1Val.Interface()) + + default: + c.RegisterParam(f.Name, d1Val.Interface()) + } } - c.RegisterParam("OS", map[string]interface{}{ - "Name": sysinfo.OS().String(), - "Version": osVersion, - "Architecture": sysinfo.Architecture().String(), - }) - c.RegisterParam("Shell", subshellName) return c } diff --git a/internal/primer/primer.go b/internal/primer/primer.go index f3cd8d8d8f..063185e604 100644 --- a/internal/primer/primer.go +++ b/internal/primer/primer.go @@ -3,7 +3,6 @@ package primer import ( "github.com/ActiveState/cli/internal/analytics" "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/constraints" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/prompt" "github.com/ActiveState/cli/internal/subshell" @@ -21,7 +20,6 @@ type Values struct { auth *authentication.Auth prompt prompt.Prompter subshell subshell.SubShell - conditional *constraints.Conditional config *config.Instance ipComm svcctl.IPCommunicator svcModel *model.SvcModel @@ -30,19 +28,18 @@ type Values struct { func New( project *project.Project, output output.Outputer, auth *authentication.Auth, prompt prompt.Prompter, - subshell subshell.SubShell, conditional *constraints.Conditional, config *config.Instance, + subshell subshell.SubShell, config *config.Instance, ipComm svcctl.IPCommunicator, svcModel *model.SvcModel, an analytics.Dispatcher) *Values { v := &Values{ - output: output, - auth: auth, - prompt: prompt, - subshell: subshell, - conditional: conditional, - config: config, - ipComm: ipComm, - svcModel: svcModel, - analytics: an, + output: output, + auth: auth, + prompt: prompt, + subshell: subshell, + config: config, + ipComm: ipComm, + svcModel: svcModel, + analytics: an, } if project != nil { v.project = project @@ -91,10 +88,6 @@ type Subsheller interface { Subshell() subshell.SubShell } -type Conditioner interface { - Conditional() *constraints.Conditional -} - func (v *Values) Project() *project.Project { return v.project } @@ -127,10 +120,6 @@ func (v *Values) SvcModel() *model.SvcModel { return v.svcModel } -func (v *Values) Conditional() *constraints.Conditional { - return v.conditional -} - func (v *Values) Config() *config.Instance { return v.config } diff --git a/internal/runners/show/show.go b/internal/runners/show/show.go index e07bbc54e2..1aa164cc28 100644 --- a/internal/runners/show/show.go +++ b/internal/runners/show/show.go @@ -7,7 +7,6 @@ import ( "github.com/go-openapi/strfmt" - "github.com/ActiveState/cli/internal/constraints" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/locale" @@ -22,7 +21,6 @@ import ( "github.com/ActiveState/cli/pkg/platform/runtime/setup" "github.com/ActiveState/cli/pkg/platform/runtime/target" "github.com/ActiveState/cli/pkg/project" - "github.com/ActiveState/cli/pkg/projectfile" ) // Params describes the data required for the show run func. @@ -32,10 +30,9 @@ type Params struct { // Show manages the show run execution context. type Show struct { - project *project.Project - out output.Outputer - conditional *constraints.Conditional - auth *authentication.Auth + project *project.Project + out output.Outputer + auth *authentication.Auth } type auther interface { @@ -45,7 +42,6 @@ type auther interface { type primeable interface { primer.Projecter primer.Outputer - primer.Conditioner primer.Auther } @@ -127,7 +123,6 @@ func New(prime primeable) *Show { return &Show{ prime.Project(), prime.Output(), - prime.Conditional(), prime.Auth(), } } @@ -181,12 +176,12 @@ func (s *Show) Run(params Params) error { projectURL = s.project.URL() branchName = s.project.BranchName() - events, err = eventsData(s.project.Source(), s.conditional) + events, err = eventsData(s.project) if err != nil { return locale.WrapError(err, "err_show_events", "Could not parse events") } - scripts, err = scriptsData(s.project.Source(), s.conditional) + scripts, err = scriptsData(s.project) if err != nil { return locale.WrapError(err, "err_show_scripts", "Could not parse scripts") } @@ -278,41 +273,23 @@ type languageRow struct { Version string `json:"version" locale:"state_show_language_version,Version"` } -func eventsData(project *projectfile.Project, conditional *constraints.Conditional) ([]string, error) { - if len(project.Events) == 0 { - return nil, nil - } - - constrained, err := constraints.FilterUnconstrained(conditional, project.Events.AsConstrainedEntities()) - if err != nil { - return nil, locale.WrapError(err, "err_event_condition", "Event has invalid conditional") - } - - es := projectfile.MakeEventsFromConstrainedEntities(constrained) +func eventsData(pj *project.Project) ([]string, error) { + es := pj.Events() var data []string for _, event := range es { - data = append(data, event.Name) + data = append(data, event.Name()) } return data, nil } -func scriptsData(project *projectfile.Project, conditional *constraints.Conditional) (map[string]string, error) { - if len(project.Scripts) == 0 { - return nil, nil - } - - constrained, err := constraints.FilterUnconstrained(conditional, project.Scripts.AsConstrainedEntities()) - if err != nil { - return nil, locale.WrapError(err, "err_script_condition", "Script has invalid conditional") - } - - scripts := projectfile.MakeScriptsFromConstrainedEntities(constrained) +func scriptsData(pj *project.Project) (map[string]string, error) { + scripts := pj.Scripts() data := make(map[string]string) for _, script := range scripts { - data[script.Name] = script.Description + data[script.Name()] = script.Description() } return data, nil diff --git a/pkg/project/expander.go b/pkg/project/expander.go index 76be15ca13..67b33a5a4d 100644 --- a/pkg/project/expander.go +++ b/pkg/project/expander.go @@ -1,23 +1,27 @@ package project import ( - "path/filepath" + "fmt" + "reflect" "regexp" "runtime" "strings" + "github.com/ActiveState/cli/internal/constants" + "github.com/ActiveState/cli/internal/constraints" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/language" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/osutils" + "github.com/ActiveState/cli/internal/rxutils" "github.com/ActiveState/cli/internal/scriptfile" - "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/projectfile" +) - "github.com/ActiveState/cli/internal/rxutils" - - "github.com/ActiveState/cli/internal/constants" - "github.com/ActiveState/cli/internal/constraints" +const ( + expandStructTag = "expand" + expandTagOptAsFunc = "asFunc" + expandTagOptIsPath = "isPath" ) type Expansion struct { @@ -47,7 +51,7 @@ func (ctx *Expansion) ApplyWithMaxDepth(s string, depth int) (string, error) { variable = groups[0] if len(groups) == 2 { - category = "toplevel" + category = TopLevelExpanderName name = groups[1] } if len(groups) > 2 { @@ -181,38 +185,6 @@ func expandPath(name string, script *Script) (string, error) { return sf.Filename(), nil } -// userExpander -func userExpander(auth *authentication.Auth, element string) string { - if element == "name" { - return auth.WhoAmI() - } - if element == "email" { - return auth.Email() - } - if element == "jwt" { - return auth.BearerToken() - } - return "" -} - -// Mixin provides expansions that are not sourced from a project file -type Mixin struct { - auth *authentication.Auth -} - -// NewMixin creates a Mixin object providing extra expansions -func NewMixin(auth *authentication.Auth) *Mixin { - return &Mixin{auth} -} - -// Expander expands mixin variables -func (m *Mixin) Expander(_ string, name string, meta string, _ bool, _ *Expansion) (string, error) { - if name == "user" { - return userExpander(m.auth, meta), nil - } - return "", nil -} - // ConstantExpander expands constants defined in the project-file. func ConstantExpander(_ string, name string, meta string, isFunction bool, ctx *Expansion) (string, error) { projectFile := ctx.Project.Source() @@ -228,41 +200,6 @@ func ConstantExpander(_ string, name string, meta string, isFunction bool, ctx * return "", nil } -// ProjectExpander expands constants defined in the project-file. -func ProjectExpander(_ string, name string, _ string, isFunction bool, ctx *Expansion) (string, error) { - if !isFunction { - return "", nil - } - - project := ctx.Project - switch name { - case "url": - return project.URL(), nil - case "commit": - return project.CommitID(), nil - case "branch": - return project.BranchName(), nil - case "owner": - return project.Namespace().Owner, nil - case "name": - return project.Namespace().Project, nil - case "namespace": - return project.Namespace().String(), nil - case "path": - path := project.Source().Path() - if path == "" { - return path, nil - } - dir := filepath.Dir(path) - if ctx.BashifyPaths { - return osutils.BashifyPath(dir) - } - return dir, nil - } - - return "", nil -} - func TopLevelExpander(variable string, name string, _ string, _ bool, ctx *Expansion) (string, error) { projectFile := ctx.Project.Source() switch name { @@ -270,6 +207,138 @@ func TopLevelExpander(variable string, name string, _ string, _ bool, ctx *Expan return projectFile.Project, nil case "lock": return projectFile.Lock, nil + default: + if v, ok := topLevelLookup[name]; ok { + return v, nil + } } return variable, nil } + +// entry manages a simple value held by a field as well as the field's metadata. +type entry struct { + asFunc bool + isPath bool + value string +} + +func newEntry(tag string, val reflect.Value) entry { + var asFunc, isPath bool + + tParts := strings.Split(tag, ",") + if len(tParts) > 1 { + if strings.Contains(tParts[1], expandTagOptAsFunc) { + asFunc = true + } + if strings.Contains(tParts[1], expandTagOptIsPath) { + isPath = true + } + } + + return entry{ + asFunc: asFunc, + isPath: isPath, + value: fmt.Sprintf("%v", val.Interface()), + } +} + +func makeEntryMap(structure reflect.Value) map[string]entry { + m := make(map[string]entry) + fields := reflect.VisibleFields(structure.Type()) + + // Work at depth 3: Vars.Struct.Struct.[Simple] + for _, f := range fields { + if !f.IsExported() { + continue + } + + d3Val := structure.FieldByIndex(f.Index) + m[strings.ToLower(f.Name)] = newEntry(f.Tag.Get(expandStructTag), d3Val) + } + + return m +} + +func makeEntryMapMap(structure reflect.Value) map[string]map[string]entry { + m := make(map[string]map[string]entry) + fields := reflect.VisibleFields(structure.Type()) + + // Work at depth 2: Vars.Struct.[Struct].Simple + for _, f := range fields { + if !f.IsExported() { + continue + } + + d2Val := structure.FieldByIndex(f.Index) + if d2Val.Kind() == reflect.Ptr { + d2Val = d2Val.Elem() + } + + switch d2Val.Type().Kind() { + // Convert type (to map) to express advanced control like tag handling. + case reflect.Struct: + m[strings.ToLower(f.Name)] = makeEntryMap(d2Val) + + // Format simple value. This is a leaf: Vars.Struct.[Simple] + // Conform to map-map, store at zero-valued key of inner map. + default: + m[strings.ToLower(f.Name)] = map[string]entry{ + "": newEntry(f.Tag.Get(expandStructTag), d2Val), + } + } + } + + return m +} + +func makeExpanderFuncFromMap(m map[string]map[string]entry) ExpanderFunc { + return func(v, name, meta string, isFunc bool, ctx *Expansion) (string, error) { + if isFunc && meta == "()" { + meta = "" + } + + if sub, ok := m[name]; ok { + if e, ok := sub[meta]; ok && isFunc == e.asFunc { + value := e.value + if ctx.BashifyPaths && e.isPath { + return osutils.BashifyPath(value) + } + + return value, nil + } + } + + return "", nil + } +} + +func makeExpanderFuncFromFunc(fn reflect.Value) ExpanderFunc { + return func(v, name, meta string, isFunc bool, ctx *Expansion) (string, error) { + // Call function; It should not require any arguments. + // Work at depth 1: Vars.[FuncReturnsSomething]... + vals := fn.Call(nil) + if len(vals) > 1 { + if !vals[1].IsNil() { + return "", vals[1].Interface().(error) + } + } + + d1Val := vals[0] + // deref if needed + if d1Val.Kind() == reflect.Ptr { + d1Val = d1Val.Elem() + } + + switch d1Val.Kind() { + // Convert type (to map-map) to express advanced control like tag handling. + case reflect.Struct: + m := makeEntryMapMap(d1Val) + expandFromMap := makeExpanderFuncFromMap(m) + return expandFromMap(v, name, meta, isFunc, ctx) + + // Format simple value. This is a leaf: Vars.[FuncReturnsSimple] + default: + return fmt.Sprintf("%v", d1Val.Interface()), nil + } + } +} diff --git a/pkg/project/expander_test.go b/pkg/project/expander_test.go index 4e98d70cbf..03f7ce2027 100644 --- a/pkg/project/expander_test.go +++ b/pkg/project/expander_test.go @@ -12,11 +12,13 @@ import ( "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" + "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/language" "github.com/ActiveState/cli/pkg/project" "github.com/ActiveState/cli/pkg/projectfile" + "github.com/ActiveState/cli/pkg/projget" ) func loadProject(t *testing.T) *project.Project { @@ -60,11 +62,15 @@ scripts: pjFile.Persist() - return project.Get() + pj, err := projget.NewProjectForTest(pjFile) + require.NoError(t, err) + + return pj } func TestExpandProject(t *testing.T) { prj := loadProject(t) + prj.Source().SetPath(fmt.Sprintf("spoofed path%sactivestate.yaml", string(os.PathSeparator))) expanded, err := project.ExpandFromProject("$project.url()", prj) @@ -97,8 +103,9 @@ func TestExpandProject(t *testing.T) { if runtime.GOOS == "windows" { prj.Source().SetPath(fmt.Sprintf(`c:\another\spoofed path\activestate.yaml`)) + expanded, err = project.ExpandFromProjectBashifyPaths("$project.path()", prj) - require.NoError(t, err) + require.NoError(t, err, errs.JoinMessage(err)) assert.Equal(t, `/c/another/spoofed\ path`, expanded) } } diff --git a/pkg/project/project.go b/pkg/project/project.go index 4eb646478e..ffd8c988dd 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -58,6 +58,13 @@ func (p *Project) SetCommit(commitID string) error { return p.Source().SetCommit(commitID, p.IsHeadless()) } +func (p *Project) SetUpdateCallback(fn func()) { + if p.projectfile == nil { + return + } + p.projectfile.SetUpdateCallback(fn) +} + // Constants returns a reference to projectfile.Constants func (p *Project) Constants() []*Constant { constrained, err := constraints.FilterUnconstrained(pConditional, p.projectfile.Constants.AsConstrainedEntities()) diff --git a/pkg/project/project_test.go b/pkg/project/project_test.go index ce68d26a96..65f2270fcf 100644 --- a/pkg/project/project_test.go +++ b/pkg/project/project_test.go @@ -7,14 +7,15 @@ import ( "testing" "github.com/ActiveState/cli/internal/config" - "github.com/ActiveState/cli/internal/constraints" "github.com/ActiveState/cli/internal/environment" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/language" + "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/subshell" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/project" "github.com/ActiveState/cli/pkg/projectfile" + "github.com/ActiveState/cli/pkg/projget" "github.com/stretchr/testify/suite" ) @@ -40,12 +41,12 @@ func (suite *ProjectTestSuite) BeforeTest(suiteName, testName string) { projectFile.Persist() suite.projectFile = projectFile suite.Require().Nil(err, "Should retrieve projectfile without issue.") - suite.project, err = project.GetSafe() - suite.Require().Nil(err, "Should retrieve project without issue.") cfg, err := config.New() suite.Require().NoError(err) - project.RegisterConditional(constraints.NewPrimeConditional(nil, suite.project, subshell.New(cfg).Shell())) + + suite.project, err = projget.NewProject(output.Get(), nil, subshell.New(cfg).Shell()) + suite.Require().Nil(err, "Should retrieve project without issue.") } func (suite *ProjectTestSuite) TestGet() { diff --git a/pkg/project/registry.go b/pkg/project/registry.go index 30f78a7d66..c4402d9bc2 100644 --- a/pkg/project/registry.go +++ b/pkg/project/registry.go @@ -1,9 +1,12 @@ package project import ( + "fmt" + "reflect" "strings" "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/locale" ) // expanderRegistry maps category names to their Expander Func implementations. @@ -12,29 +15,135 @@ var expanderRegistry = map[string]ExpanderFunc{} var ( ErrExpandBadName = errs.New("Bad expander name") ErrExpandNoFunc = errs.New("Expander has no handler") + topLevelLookup = make(map[string]string) ) -const TopLevelExpanderName = "toplevel" +const ( + TopLevelExpanderName = "toplevel" +) func init() { expanderRegistry = map[string]ExpanderFunc{ "events": EventExpander, "scripts": ScriptExpander, "constants": ConstantExpander, - "project": ProjectExpander, TopLevelExpanderName: TopLevelExpander, } } +func RegisterTopLevelStruct(name string, val interface{}) error { + v := reflect.ValueOf(val) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + m := makeEntryMapMap(v) + name = strings.ToLower(name) + err := RegisterExpander(name, makeExpanderFuncFromMap(m)) + if err != nil { + return locale.WrapError( + err, "project_expand_register_expander_map", + "Cannot register expander (map)", + ) + } + + return nil +} + +func RegisterTopLevelFunc(name string, val interface{}) error { + v := reflect.ValueOf(val) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + name = strings.ToLower(name) + err := RegisterExpander(name, makeExpanderFuncFromFunc(v)) + if err != nil { + return locale.WrapError( + err, "project_expand_register_expander_func", + "Cannot register expander (func)", + ) + } + + return nil +} + +func RegisterTopLevelStringer(name string, val interface{}) error { + v := reflect.ValueOf(val) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + topLevelLookup[strings.ToLower(name)] = fmt.Sprintf("%v", v.Interface()) + + return nil +} + +/*func RegisterStruct(val interface{}) error { + v := reflect.ValueOf(val) + // deref if needed + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + fields := reflect.VisibleFields(v.Type()) + + // Work at depth 1: Vars.[Struct].Struct.Simple + for _, f := range fields { + if !f.IsExported() { + continue + } + + d1Val := v.FieldByIndex(f.Index) + if d1Val.Kind() == reflect.Ptr { + d1Val = d1Val.Elem() + } + + // If function registration is needed at greater depths, this + // will need to be reworked (and may not be possible without + // expansive refactoring). + switch d1Val.Type().Kind() { + // Convert type (to map-map) to express advanced control like tag handling. + case reflect.Struct: + m := makeEntryMapMap(d1Val) + name := strings.ToLower(f.Name) + err := RegisterExpander(name, makeExpanderFuncFromMap(m)) + if err != nil { + return locale.WrapError( + err, "project_expand_register_expander_map", + "Cannot register expander (map)", + ) + } + + // Expand from function. + case reflect.Func: + name := strings.ToLower(f.Name) + err := RegisterExpander(name, makeExpanderFuncFromFunc(d1Val)) + if err != nil { + return locale.WrapError( + err, "project_expand_register_expander_func", + "Cannot register expander (func)", + ) + } + + // Format simple value. This is a leaf: Vars.[Simple] + default: + topLevelLookup[strings.ToLower(f.Name)] = fmt.Sprintf("%v", d1Val.Interface()) + } + } + + return nil +}*/ + // RegisterExpander registers an Expander Func for some given handler value. The handler value // must not effectively be a blank string and the Func must be defined. It is definitely possible // to replace an existing handler using this function. func RegisterExpander(handle string, expanderFn ExpanderFunc) error { cleanHandle := strings.TrimSpace(handle) if cleanHandle == "" { - return errs.Wrap(ErrExpandBadName, "secrets_expander_err_empty_name") + return locale.WrapError(ErrExpandBadName, "secrets_expander_err_empty_name") } else if expanderFn == nil { - return errs.Wrap(ErrExpandNoFunc, "secrets_expander_err_undefined") + return locale.WrapError(ErrExpandNoFunc, "secrets_expander_err_undefined") } expanderRegistry[cleanHandle] = expanderFn return nil diff --git a/pkg/projectfile/projectfile.go b/pkg/projectfile/projectfile.go index 1e02af3ed9..ef6429e473 100644 --- a/pkg/projectfile/projectfile.go +++ b/pkg/projectfile/projectfile.go @@ -104,6 +104,8 @@ type Project struct { parsedURL projectURL // parsed url data parsedBranch string parsedVersion string + + updateCallback func() } // Build covers the build map, which can go under languages or packages @@ -558,11 +560,6 @@ func (p *Project) Path() string { return p.path } -// SetPath sets the path of the project file and should generally only be used by tests -func (p *Project) SetPath(path string) { - p.path = path -} - // VersionBranch returns the branch as it was interpreted from the lock func (p *Project) VersionBranch() string { return p.parsedBranch @@ -687,8 +684,21 @@ func (p *Project) save(cfg ConfigGetter, path string) error { return nil } +func (p *Project) runUpdateCallback() { + if p.updateCallback == nil { + return + } + p.updateCallback() +} + +func (p *Project) SetUpdateCallback(fn func()) { + p.updateCallback = fn +} + // SetNamespace updates the namespace in the project file func (p *Project) SetNamespace(owner, project string) error { + defer p.runUpdateCallback() + pf := NewProjectField() if err := pf.LoadProject(p.Project); err != nil { return errs.Wrap(err, "Could not load activestate.yaml") @@ -710,6 +720,8 @@ func (p *Project) SetNamespace(owner, project string) error { // in-place so that line order is preserved. // If headless is true, the project is defined by a commit-id only func (p *Project) SetCommit(commitID string, headless bool) error { + defer p.runUpdateCallback() + pf := NewProjectField() if err := pf.LoadProject(p.Project); err != nil { return errs.Wrap(err, "Could not load activestate.yaml") @@ -727,6 +739,8 @@ func (p *Project) SetCommit(commitID string, headless bool) error { // SetBranch sets the branch within the current project file. This is done // in-place so that line order is preserved. func (p *Project) SetBranch(branch string) error { + defer p.runUpdateCallback() + pf := NewProjectField() if err := pf.LoadProject(p.Project); err != nil { @@ -746,6 +760,13 @@ func (p *Project) SetBranch(branch string) error { return nil } +// SetPath sets the path of the project file and should generally only be used by tests +func (p *Project) SetPath(path string) { + defer p.runUpdateCallback() + + p.path = path +} + // GetProjectFilePath returns the path to the project activestate.yaml func GetProjectFilePath() (string, error) { defer profile.Measure("GetProjectFilePath", time.Now()) diff --git a/pkg/projectfile/vars/vars.go b/pkg/projectfile/vars/vars.go new file mode 100644 index 0000000000..cb81333c22 --- /dev/null +++ b/pkg/projectfile/vars/vars.go @@ -0,0 +1,143 @@ +// Package vars provides a single type expressing the data accessible by the +// activestate.yaml for conditionals and variable expansions. +// +// The structure should not grow beyond a depth of 3. That is, .OS.Version.Major +// is fine, but .OS.Version.Major.Something is not. External (leaf) nodes must +// be able to resolve to a string using `fmt.Sprintf("%v")`. Keep in mind that +// the Vars type itself is depth 0, so it does not count for depth, and is +// represented in the activestate.yaml as either the first `.` or the `$`. +// +// Nodes at depth 1 may be a function, but the return value must also resolve +// to a string using `fmt.Sprintf("%v")`. A second return value of `error` is +// allowed. For variable expansion, a non-function node may be tagged as a +// function (asFunc) so that it must be called using parenthesis +// (`$project.name()`). +// +// Path nodes should be tagged (isPath) so that bashification of the path is +// applied when necessary. +package vars + +import ( + "path/filepath" + + "github.com/ActiveState/cli/internal/multilog" + "github.com/ActiveState/cli/internal/rtutils/p" + "github.com/ActiveState/cli/pkg/platform/authentication" + "github.com/ActiveState/cli/pkg/sysinfo" +) + +type projectDataProvider interface { + Owner() string + Name() string + NamespaceString() string + CommitID() string + BranchName() string + Path() string + URL() string +} + +type Project struct { + Namespace string `expand:",asFunc"` + Name string `expand:",asFunc"` + Owner string `expand:",asFunc"` + Url string `expand:",asFunc"` + Commit string `expand:",asFunc"` + Branch string `expand:",asFunc"` + Path string `expand:",asFunc;isPath"` + + // legacy fields + NamespacePrefix string +} + +func NewProject(pj projectDataProvider) *Project { + var ( + project = &Project{} + ) + if !p.IsNil(pj) { + project.Namespace = pj.NamespaceString() + project.Name = pj.Name() + project.Owner = pj.Owner() + project.Url = pj.URL() + project.Commit = pj.CommitID() + project.Branch = pj.BranchName() + project.Path = pj.Path() + if project.Path != "" { + project.Path = filepath.Dir(project.Path) + } + + project.NamespacePrefix = pj.NamespaceString() + } + + return project +} + +type OSVersion struct { + Name string + Version string + Major int + Minor int + Micro int +} + +type OS struct { + Name string + Version OSVersion + Architecture string +} + +func NewOS(osVersion *sysinfo.OSVersionInfo) *OS { + return &OS{ + Name: sysinfo.OS().String(), + Version: OSVersion{ + Name: osVersion.Name, + Version: osVersion.Version, + Major: osVersion.Major, + Minor: osVersion.Minor, + Micro: osVersion.Micro, + }, + Architecture: sysinfo.Architecture().String(), + } +} + +type User struct { + Name string + Email string + JWT string +} + +type Mixin struct { + auth *authentication.Auth + User *User +} + +func NewMixin(auth *authentication.Auth) *Mixin { + return &Mixin{ + auth: auth, + User: &User{ + Name: auth.WhoAmI(), + Email: auth.Email(), + JWT: auth.BearerToken(), + }, + } +} + +type Vars struct { + Project *Project + OS *OS + Shell string + Mixin func() *Mixin +} + +func New(auth *authentication.Auth, project *Project, subshellName string) *Vars { + osVersion, err := sysinfo.OSVersion() + if err != nil { + multilog.Error("Could not detect OSVersion: %v", err) + } + + return &Vars{ + Project: project, + OS: NewOS(osVersion), + Shell: subshellName, + Mixin: func() *Mixin { return NewMixin(auth) }, + } +} diff --git a/pkg/projget/projget.go b/pkg/projget/projget.go new file mode 100644 index 0000000000..94ff9441f0 --- /dev/null +++ b/pkg/projget/projget.go @@ -0,0 +1,60 @@ +package projget + +import ( + "github.com/ActiveState/cli/internal/constraints" + "github.com/ActiveState/cli/internal/errs" + "github.com/ActiveState/cli/internal/output" + "github.com/ActiveState/cli/pkg/platform/authentication" + "github.com/ActiveState/cli/pkg/project" + "github.com/ActiveState/cli/pkg/projectfile" + "github.com/ActiveState/cli/pkg/projectfile/vars" +) + +func NewProject(out output.Outputer, auth *authentication.Auth, shell string) (*project.Project, error) { + var pjf *projectfile.Project + + // Retrieve project file + pjPath, err := projectfile.GetProjectFilePath() + if err != nil && errs.Matches(err, &projectfile.ErrorNoProjectFromEnv{}) { + // Fail if we are meant to inherit the projectfile from the environment, but the file doesn't exist + return nil, err + } + + if pjPath != "" { + pjf, err = projectfile.FromPath(pjPath) + if err != nil { + return nil, err + } + } + + return newProject(out, auth, shell, pjf) +} + +func newProject(out output.Outputer, auth *authentication.Auth, shell string, pjf *projectfile.Project) (*project.Project, error) { + if pjf == nil { + return nil, nil + } + + pj, err := project.New(pjf, out) + if err != nil { + return nil, err + } + + registerProjectVars := func() { + projVars := vars.New(auth, vars.NewProject(pj), shell) + conditional := constraints.NewPrimeConditional(projVars) + project.RegisterConditional(conditional) + _ = project.RegisterTopLevelStruct("project", projVars.Project) + _ = project.RegisterTopLevelFunc("mixin", projVars.Mixin) + _ = project.RegisterTopLevelStringer("shell", projVars.Shell) + } + + pj.SetUpdateCallback(registerProjectVars) + registerProjectVars() + + return pj, nil +} + +func NewProjectForTest(pjf *projectfile.Project) (*project.Project, error) { + return newProject(output.Get(), nil, "noshell", pjf) +} diff --git a/pkg/sysinfo/sysinfo.go b/pkg/sysinfo/sysinfo.go index e407e58819..b822e53f02 100644 --- a/pkg/sysinfo/sysinfo.go +++ b/pkg/sysinfo/sysinfo.go @@ -211,4 +211,4 @@ func parseVersionInfo(v string) (*VersionInfo, error) { Minor: minor, Micro: micro, }, nil -} \ No newline at end of file +} diff --git a/scripts/ci/parallelize/parallelize.go b/scripts/ci/parallelize/parallelize.go index 24360ace9a..c7b6a2e26a 100644 --- a/scripts/ci/parallelize/parallelize.go +++ b/scripts/ci/parallelize/parallelize.go @@ -17,6 +17,7 @@ import ( "github.com/ActiveState/cli/internal/installation/storage" "github.com/ActiveState/cli/internal/osutils" "github.com/ActiveState/cli/pkg/project" + "github.com/ActiveState/cli/pkg/projectfile/vars" "github.com/gammazero/workerpool" ) @@ -35,7 +36,7 @@ func main() { } func run() error { - if len(os.Args) <= 1{ + if len(os.Args) <= 1 { return errs.New("Must provide single argument with JSON blob, or [job ] to check the results of a job.") } @@ -104,7 +105,8 @@ func runJob(job Job) { return } - cond := constraints.NewPrimeConditional(nil, pj, "") + projVars := vars.New(nil, vars.NewProject(pj), "noshell") + cond := constraints.NewPrimeConditional(projVars) run, err := cond.Eval(job.If) if err != nil { failure("Could not evaluate conditonal: %s, error: %s\n", job.If, errs.JoinMessage(err)) @@ -120,8 +122,7 @@ func runJob(job Job) { return } - - code, _, err := exeutils.Execute(job.Args[0] + osutils.ExeExt, job.Args[1:], func(cmd *exec.Cmd) error { + code, _, err := exeutils.Execute(job.Args[0]+osutils.ExeExt, job.Args[1:], func(cmd *exec.Cmd) error { cmd.Stdout = outfile cmd.Stderr = outfile cmd.Env = append(job.Env, os.Environ()...) @@ -136,14 +137,14 @@ func runJob(job Job) { func readJob(id string) error { jobfile := filepath.Join(jobDir(), fmt.Sprintf("%s.out", id)) - if ! fileutils.FileExists(jobfile) { + if !fileutils.FileExists(jobfile) { return errs.New("Job does not exist: %s", jobfile) } contents := strings.Split(string(fileutils.ReadFileUnsafe(jobfile)), "\n") code, err := strconv.Atoi(contents[len(contents)-1]) if err != nil { - return errs.Wrap(err,"Expected last line to be the exit code, instead found: %s", contents[len(contents)-1]) + return errs.Wrap(err, "Expected last line to be the exit code, instead found: %s", contents[len(contents)-1]) } fmt.Println(strings.Join(contents[0:(len(contents)-2)], "\n"))