diff --git a/cmd/state-installer/cmd.go b/cmd/state-installer/cmd.go index 58d3b7556e..7ad8b0ae1e 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 6320c67d0f..49f70f5f3e 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 7189d0a00e..07d384291a 100644 --- a/cmd/state-svc/main.go +++ b/cmd/state-svc/main.go @@ -111,7 +111,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 e0fc4e650a..3fd8cc8ef4 100644 --- a/cmd/state/main.go +++ b/cmd/state/main.go @@ -11,14 +11,12 @@ import ( "strings" "time" - "github.com/ActiveState/cli/cmd/state/internal/cmdtree/exechandlers/messenger" - "github.com/ActiveState/cli/internal/captain" - "github.com/ActiveState/cli/cmd/state/internal/cmdtree" + "github.com/ActiveState/cli/cmd/state/internal/cmdtree/exechandlers/messenger" 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" @@ -40,7 +38,6 @@ 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" ) func main() { @@ -169,24 +166,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 := project.NewWithVars(out, auth, sshell.Shell()) + if err != nil { + return err } pjNamespace := "" @@ -194,9 +181,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()) } @@ -211,16 +195,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 46d5e48e98..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/ptr" - "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 !ptr.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 387a2445c5..9af9769965 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 } @@ -128,7 +124,6 @@ func New(prime primeable) *Show { return &Show{ prime.Project(), prime.Output(), - prime.Conditional(), prime.Auth(), } } @@ -182,12 +177,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..ec9712e9ee 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,95 +185,178 @@ 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() +// 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() + constrained, err := constraints.FilterUnconstrained(pConditional, projectFile.Constants.AsConstrainedEntities()) + if err != nil { + return "", err } - if element == "email" { - return auth.Email() + for _, v := range constrained { + if v.ID() == name { + return projectfile.MakeConstantsFromConstrainedEntities([]projectfile.ConstrainedEntity{v})[0].Value, nil + } } - if element == "jwt" { - return auth.BearerToken() + return "", nil +} + +func TopLevelExpander(variable string, name string, _ string, _ bool, ctx *Expansion) (string, error) { + projectFile := ctx.Project.Source() + switch name { + case "project": + return projectFile.Project, nil + case "lock": + return projectFile.Lock, nil + default: + if v, ok := topLevelLookup[name]; ok { + return v, nil + } } - return "" + return variable, nil } -// Mixin provides expansions that are not sourced from a project file -type Mixin struct { - auth *authentication.Auth +// 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 } -// NewMixin creates a Mixin object providing extra expansions -func NewMixin(auth *authentication.Auth) *Mixin { - return &Mixin{auth} +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()), + } } -// 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 +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 "", nil + + return m } -// 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() - constrained, err := constraints.FilterUnconstrained(pConditional, projectFile.Constants.AsConstrainedEntities()) - if err != nil { - return "", err +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), + } + } } - for _, v := range constrained { - if v.ID() == name { - return projectfile.MakeConstantsFromConstrainedEntities([]projectfile.ConstrainedEntity{v})[0].Value, nil + + return m +} + +func makeLazyExpanderFuncFromPtrToStruct(val reflect.Value) ExpanderFunc { + // This function's args maintain scope across multiple calls; Do not overwrite the args. + return func(v, name, meta string, isFunc bool, ctx *Expansion) (string, error) { + iface := val.Interface() + if u, ok := iface.(interface{ Update(*Project) }); ok { + u.Update(ctx.Project) } + + valDeref := val + if valDeref.Kind() == reflect.Ptr { + valDeref = valDeref.Elem() + } + fn := makeExpanderFuncFromMap(makeEntryMapMap(valDeref)) + + return fn(v, name, meta, isFunc, 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 { +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 } +} - 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 +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) + } } - dir := filepath.Dir(path) - if ctx.BashifyPaths { - return osutils.BashifyPath(dir) + + d1Val := vals[0] + // deref if needed + if d1Val.Kind() == reflect.Ptr { + d1Val = d1Val.Elem() } - return dir, nil - } - return "", nil -} + 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) -func TopLevelExpander(variable string, name string, _ string, _ bool, ctx *Expansion) (string, error) { - projectFile := ctx.Project.Source() - switch name { - case "project": - return projectFile.Project, nil - case "lock": - return projectFile.Lock, nil + // Format simple value. This is a leaf: Vars.[FuncReturnsSimple] + default: + return fmt.Sprintf("%v", d1Val.Interface()), nil + } } - return variable, nil } diff --git a/pkg/project/expander_test.go b/pkg/project/expander_test.go index 4e98d70cbf..05d4d1d880 100644 --- a/pkg/project/expander_test.go +++ b/pkg/project/expander_test.go @@ -12,6 +12,7 @@ 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" @@ -60,11 +61,15 @@ scripts: pjFile.Persist() - return project.Get() + pj, err := project.NewWithVarsForTest(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 +102,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..bc5ed2d4df 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -28,8 +28,10 @@ import ( // Build covers the build structure type Build map[string]string -var pConditional *constraints.Conditional -var normalizeRx *regexp.Regexp +var ( + pConditional *constraints.Conditional + normalizeRx *regexp.Regexp +) func init() { var err error @@ -274,6 +276,50 @@ func NewLegacy(p *projectfile.Project) (*Project, error) { return New(p, output.Get()) } +func NewWithVars(out output.Outputer, auth *authentication.Auth, shell string) (*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 newWithVars(out, auth, shell, pjf) +} + +func newWithVars(out output.Outputer, auth *authentication.Auth, shell string, pjf *projectfile.Project) (*Project, error) { + if pjf == nil { + return nil, nil + } + + pj, err := New(pjf, out) + if err != nil { + return nil, err + } + + projVars := NewVars(auth, pj, shell) + conditional := constraints.NewPrimeConditional(projVars) + RegisterConditional(conditional) + if err := RegisterStruct(projVars); err != nil { + return nil, err + } + + return pj, nil +} + +func NewWithVarsForTest(pjf *projectfile.Project) (*Project, error) { + return newWithVars(output.Get(), nil, "not set", pjf) +} + // Parse will parse the given projectfile and instantiate a Project struct with it func Parse(fpath string) (*Project, error) { pjfile, err := projectfile.Parse(fpath) diff --git a/pkg/project/project_test.go b/pkg/project/project_test.go index bfd4ff5bda..403c3bf518 100644 --- a/pkg/project/project_test.go +++ b/pkg/project/project_test.go @@ -7,10 +7,10 @@ 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" @@ -40,12 +40,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 = project.NewWithVars(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..b4e6caba3a 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,87 @@ 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 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) + d1ValOrig := d1Val + 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: + name := strings.ToLower(f.Name) + err := RegisterExpander(name, makeLazyExpanderFuncFromPtrToStruct(d1ValOrig)) + 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/project/vars.go b/pkg/project/vars.go new file mode 100644 index 0000000000..e31048e85f --- /dev/null +++ b/pkg/project/vars.go @@ -0,0 +1,131 @@ +package project + +import ( + "path/filepath" + + "github.com/ActiveState/cli/internal/multilog" + "github.com/ActiveState/cli/pkg/platform/authentication" + "github.com/ActiveState/cli/pkg/sysinfo" +) + +// 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. + +type Projvars 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 NewProjvars(pj *Project) *Projvars { + p := &Projvars{} + p.Update(pj) + return p +} + +func (p *Projvars) Update(pj *Project) { + p.Namespace = pj.NamespaceString() + p.Name = pj.Name() + p.Owner = pj.Owner() + p.Url = pj.URL() + p.Commit = pj.CommitID() + p.Branch = pj.BranchName() + p.Path = pj.Path() + if p.Path != "" { + p.Path = filepath.Dir(p.Path) + } + p.NamespacePrefix = pj.NamespaceString() +} + +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 *Projvars + OS *OS + Shell string + Mixin func() *Mixin +} + +func NewVars(auth *authentication.Auth, pj *Project, subshellName string) *Vars { + osVersion, err := sysinfo.OSVersion() + if err != nil { + multilog.Error("Could not detect OSVersion: %v", err) + } + + return &Vars{ + Project: NewProjvars(pj), + OS: NewOS(osVersion), + Shell: subshellName, + Mixin: func() *Mixin { return NewMixin(auth) }, + } +} 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..cf21bf17b4 100644 --- a/scripts/ci/parallelize/parallelize.go +++ b/scripts/ci/parallelize/parallelize.go @@ -35,7 +35,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 +104,8 @@ func runJob(job Job) { return } - cond := constraints.NewPrimeConditional(nil, pj, "") + projVars := project.NewVars(nil, 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 +121,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 +136,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"))