Skip to content

Commit ced45ff

Browse files
Revert "feat(list): runnables carry the tool they are offered as, and each one's grant (#1207)" (#1210)
This reverts commit ad957ae. Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
1 parent 9a30f6e commit ced45ff

9 files changed

Lines changed: 95 additions & 810 deletions

File tree

‎cmd/generate/runnable_bindings.go‎

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"slices"
1111
"sort"
1212
"strings"
13+
"time"
1314

1415
"github.com/codefly-dev/cli/cmd/common"
1516
"github.com/codefly-dev/cli/pkg/cli"
@@ -475,12 +476,19 @@ func SelectRunnableBindings(ctx context.Context, workspace *resources.Workspace,
475476
return selection, nil
476477
}
477478

478-
// RunnableBindingKey is the configuration key an operation is bound under.
479-
// The spelling belongs to pkg/runnables, because the listing that reports what
480-
// an environment has answered must key by exactly what this writes; this stays
481-
// as the name generation's own call sites and tests read by.
479+
// RunnableBindingKey is the configuration key an operation is bound under:
480+
// its module and its derived name, upper-cased with every other character an
481+
// underscore, so the SDK's workspace-value lookup finds it.
482482
func RunnableBindingKey(module, name string) string {
483-
return runnablespkg.BindingKey(module, name)
483+
var key strings.Builder
484+
for _, r := range strings.ToUpper(module + "__" + name) {
485+
if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
486+
key.WriteRune(r)
487+
continue
488+
}
489+
key.WriteByte('_')
490+
}
491+
return key.String()
484492
}
485493

486494
// prepareRunnable prepares one derived operation for the environment the
@@ -507,7 +515,7 @@ func prepareRunnable(ctx context.Context, module *resources.Module, operation *r
507515
if kind == composition.APIContractKindOpenAPI {
508516
codes = corerunnable.HTTPStatusCodes
509517
}
510-
spec, err := operation.Operation.Spec(descriptor.GetOperation(), codes)
518+
spec, err := operationSpec(operation.Operation, descriptor.GetOperation(), codes)
511519
if err != nil {
512520
return nil, err
513521
}
@@ -532,6 +540,65 @@ func prepareRunnable(ctx context.Context, module *resources.Module, operation *r
532540
// operationSpec reads the derived document back into Core's spec so Core can
533541
// resolve its slots once and write the resulting policy with Policy(). The
534542
// document round-trip test holds both conversions to every Operation field.
543+
func operationSpec(declared *runnablespkg.Operation, spelling string, codes corerunnable.CodeVocabulary) (*corerunnable.OperationSpec, error) {
544+
if declared == nil {
545+
return nil, errors.New("the operation declares no execution policy")
546+
}
547+
durations := map[string]time.Duration{}
548+
for field, spelled := range map[string]string{
549+
"attempt_timeout": declared.AttemptTimeout,
550+
"total_timeout": declared.TotalTimeout,
551+
"backoff": declared.Backoff,
552+
} {
553+
parsed, err := time.ParseDuration(spelled)
554+
if err != nil {
555+
return nil, fmt.Errorf("%s %q is not a duration: %w", field, spelled, err)
556+
}
557+
durations[field] = parsed
558+
}
559+
completion, named := basev0.RunnableExecution_Completion_value[declared.Completion]
560+
if !named || basev0.RunnableExecution_Completion(completion) == basev0.RunnableExecution_COMPLETION_UNKNOWN {
561+
return nil, fmt.Errorf("completion %q is not a completion mode; %s and %s are the two",
562+
declared.Completion, basev0.RunnableExecution_COMPLETION_CALL, basev0.RunnableExecution_COMPLETION_SUBMIT)
563+
}
564+
var recovery basev0.RunnableExecution_Recovery
565+
if declared.Recovery != "" {
566+
value, known := basev0.RunnableExecution_Recovery_value[declared.Recovery]
567+
if !known || basev0.RunnableExecution_Recovery(value) == basev0.RunnableExecution_RECOVERY_UNKNOWN {
568+
return nil, fmt.Errorf("recovery %q is not a recovery; %s and %s are the two",
569+
declared.Recovery, basev0.RunnableExecution_RECOVERY_RECOMPUTE, basev0.RunnableExecution_RECOVERY_RECEIPT)
570+
}
571+
recovery = basev0.RunnableExecution_Recovery(value)
572+
}
573+
return &corerunnable.OperationSpec{
574+
Method: spelling,
575+
Codes: codes,
576+
AttemptTimeout: durations["attempt_timeout"],
577+
TotalTimeout: durations["total_timeout"],
578+
MaxAttempts: declared.MaxAttempts,
579+
Backoff: durations["backoff"],
580+
RetryableCodes: declared.RetryableCodes,
581+
Audience: declared.Audience,
582+
InvokeScopes: preparedScopes(declared.InvokeScopes),
583+
LookupScopes: preparedScopes(declared.LookupScopes),
584+
LookupMethod: declared.LookupMethod,
585+
MaxInputBytes: declared.MaxInputBytes,
586+
MaxOutputBytes: declared.MaxOutputBytes,
587+
Completion: basev0.RunnableExecution_Completion(completion),
588+
Tool: declared.Tool,
589+
RequiredScopeSlots: declared.RequiredScopeSlots,
590+
Recovery: recovery,
591+
}, nil
592+
}
593+
594+
func preparedScopes(declared []runnablespkg.Scope) []*basev0.WorkScopeV1 {
595+
scopes := make([]*basev0.WorkScopeV1, 0, len(declared))
596+
for _, scope := range declared {
597+
scopes = append(scopes, &basev0.WorkScopeV1{ResourceKind: scope.ResourceKind, Actions: scope.Actions, ResourceIds: scope.ResourceIDs})
598+
}
599+
return scopes
600+
}
601+
535602
// verifyPublishedContract holds the owner's published contract to the bytes the
536603
// API contract catalog recorded for it. A prepared value states the digest of
537604
// the bounded contract derived from it, so a catalog that no longer describes

‎cmd/generate/runnable_scopes_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestOperationDocumentRoundTripsEveryCoreField(t *testing.T) {
6969
require.Contains(t, fields, string(field.Name()), "carry every Core field through the document")
7070
}
7171
require.Len(t, fields, coreFields.Len()+1, "only method is document-specific")
72-
spec, err := document.Spec(document.Method, corerunnable.GRPCStatusNames)
72+
spec, err := operationSpec(document, document.Method, corerunnable.GRPCStatusNames)
7373
require.NoError(t, err)
7474
require.True(t, proto.Equal(declared, spec.Policy()), "declared %v; read %v", declared, spec.Policy())
7575
// Check the Go shape too, so an omitempty zero cannot hide a new field.

‎cmd/list/runnables.go‎

Lines changed: 21 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
package list
22

33
import (
4-
"context"
54
"encoding/json"
65
"fmt"
7-
"sort"
86
"strings"
97
"text/tabwriter"
108

119
"github.com/codefly-dev/cli/cmd/common"
12-
"github.com/codefly-dev/cli/pkg/environments"
1310
runnablespkg "github.com/codefly-dev/cli/pkg/runnables"
1411
"github.com/codefly-dev/core/resources"
1512
"github.com/spf13/cobra"
@@ -18,8 +15,6 @@ import (
1815
var (
1916
listRunnablesModule string
2017
listRunnablesJSON bool
21-
listRunnablesEnv string
22-
listRunnablesTools bool
2318
)
2419

2520
// RunnablesCmd lists the runnables declared in the current workspace.
@@ -31,64 +26,16 @@ var RunnablesCmd = &cobra.Command{
3126
A runnable is identified by module/name @ version: a version is an immutable
3227
release and several versions of one name coexist.
3328
34-
Every Runnable a module derives from its published contracts is listed beside
35-
the ones it declares. With --tools or --env, each row also carries the tool its
36-
owner exposed it as — the model-facing name and the explicit effect. An
37-
operation with no exposure is not a tool and is shown without one: absent means
38-
not exposed. --tools keeps only the exposed ones.
39-
40-
With --env, each derived operation also carries the configuration key its
41-
authority is answered under (` + "`runnable-scope-selections`" + ` in
42-
workspace.codefly.yaml) and what that environment has answered, judged by the
43-
rules ` + "`generate runnable-bindings`" + ` judges it by:
44-
45-
selected its answer resolves every slot the owner declared, or it declares none
46-
declined answered [] — installed for no resource, granted no authority
47-
needs-scopes its owner asks for scope slots nobody has answered
48-
invalid generation refuses this answer, and the row says why
49-
50-
An answer naming no operation at all is reported beside the rows rather than
51-
dropped, because a mistyped key answers no question and leaves its operation
52-
reading as unanswered.
53-
5429
Examples:
5530
codefly list runnables
5631
codefly list runnables --module=backend
57-
codefly list runnables --tools --env=staging
5832
codefly list runnables --json`,
5933
Args: cobra.NoArgs,
6034
RunE: func(cmd *cobra.Command, _ []string) error {
6135
return listRunnables(cmd)
6236
},
6337
}
6438

65-
// runnableRow is one listed runnable. The tool and the grant live here rather
66-
// than on the shared projection because they are this listing's question: a
67-
// field on runnablespkg.Identity would also appear in `show runnable`, whose
68-
// report already states the operation's own exposure.
69-
type runnableRow struct {
70-
runnablespkg.Identity
71-
// Tool is the exposure the owner declared, nil when the operation is no
72-
// tool. Absent means not exposed: that is the owner's fail-closed default
73-
// and this listing repeats it rather than inventing one.
74-
Tool *runnablespkg.Tool `json:"tool,omitempty"`
75-
// Grant is how the environment answers this operation, set only when a
76-
// listing was asked about one.
77-
Grant *runnablespkg.Grant `json:"grant,omitempty"`
78-
}
79-
80-
// runnableListing is what a listing asked about an environment answers with:
81-
// the rows, and the answers that match no operation. Without --env there is no
82-
// environment to have answered anything, and the listing stays the array it
83-
// has always been.
84-
type runnableListing struct {
85-
Runnables []runnableRow `json:"runnables"`
86-
// UnansweredKeys are the runnable-scope-selections keys of this
87-
// environment that no listed operation binds. Generation refuses each by
88-
// name; a listing that only walked the catalogue would never show them.
89-
UnansweredKeys []string `json:"unanswered_keys,omitempty"`
90-
}
91-
9239
func listRunnables(cmd *cobra.Command) error {
9340
ctx, done := common.NewContext()
9441
defer done()
@@ -110,149 +57,51 @@ func listRunnables(cmd *cobra.Command) error {
11057
return fmt.Errorf("cannot load modules: %w", err)
11158
}
11259

113-
// The environment is resolved once, before any module is read: a listing
114-
// asked about an environment the workspace does not declare answers with
115-
// that, rather than with rows whose grant column is silently absent.
116-
var env *environments.Environment
117-
if listRunnablesEnv != "" {
118-
if env, err = environments.Select(workspace, listRunnablesEnv); err != nil {
119-
return fmt.Errorf("cannot list the grants of environment %q: %w", listRunnablesEnv, err)
120-
}
121-
}
122-
123-
listing, err := runnableListingOf(ctx, modules, env)
124-
if err != nil {
125-
return err
126-
}
127-
128-
if listRunnablesJSON {
129-
if env == nil {
130-
return json.NewEncoder(cmd.OutOrStdout()).Encode(listing.Runnables)
131-
}
132-
return json.NewEncoder(cmd.OutOrStdout()).Encode(listing)
133-
}
134-
if len(listing.Runnables) == 0 && len(listing.UnansweredKeys) == 0 {
135-
fmt.Fprintf(cmd.OutOrStdout(), "No runnables in workspace <%s>\n", workspace.Name)
136-
return nil
137-
}
138-
return writeRunnables(cmd, listing, env)
139-
}
140-
141-
// runnableListingOf is every runnable the modules hold — the ones they declare
142-
// and the operations they derive — and, when an environment was named, how it
143-
// answers each derived operation and which of its answers match nothing.
144-
func runnableListingOf(ctx context.Context, modules []*resources.Module, env *environments.Environment) (*runnableListing, error) {
145-
listing := &runnableListing{Runnables: make([]runnableRow, 0)}
146-
bound := map[string]bool{}
60+
entries := make([]runnablespkg.Identity, 0)
14761
for _, mod := range modules {
148-
runnables, err := mod.LoadRunnables(ctx)
149-
if err != nil {
150-
return nil, fmt.Errorf("cannot load runnables: %w", err)
62+
runnables, loadErr := mod.LoadRunnables(ctx)
63+
if loadErr != nil {
64+
return fmt.Errorf("cannot load runnables: %w", loadErr)
15165
}
15266
for _, r := range runnables {
153-
listing.Runnables = append(listing.Runnables, runnableRow{Identity: runnablespkg.NewIdentity(r)})
67+
entries = append(entries, runnablespkg.NewIdentity(r))
15468
}
15569
// A module's derived operations are runnables it can be asked to
15670
// perform just as much as the ones it declares, so one listing
15771
// answers the question without the caller knowing which is which.
158-
derived, err := runnablespkg.LoadDerivedOperations(mod.Dir())
159-
if err != nil {
160-
return nil, fmt.Errorf("cannot load the derived runnables of module %s: %w", mod.Name, err)
72+
derived, derivedErr := runnablespkg.LoadDerivedOperations(mod.Dir())
73+
if derivedErr != nil {
74+
return fmt.Errorf("cannot load the derived runnables of module %s: %w", mod.Name, derivedErr)
16175
}
16276
for i := range derived {
163-
row := runnableRow{Identity: derived[i].Identity(), Tool: derived[i].ToolExposure()}
164-
if env != nil {
165-
// ScopeSelections is core's own ScopeSelection messages: the
166-
// environment decoded them, so the rules are applied to what
167-
// an installation would carry, not to a re-parse of the YAML.
168-
selections, answered := env.RunnableScopeSelections[runnablespkg.BindingKey(mod.Name, derived[i].Entry.Name)]
169-
row.Grant = runnablespkg.GrantOf(mod.Name, &derived[i], selections, answered)
170-
bound[row.Grant.Key] = true
171-
}
172-
listing.Runnables = append(listing.Runnables, row)
77+
entries = append(entries, derived[i].Identity())
17378
}
17479
}
175-
if env != nil {
176-
answers := make([]string, 0, len(env.RunnableScopeSelections))
177-
for key := range env.RunnableScopeSelections {
178-
answers = append(answers, key)
179-
}
180-
sort.Strings(answers)
181-
listing.UnansweredKeys = runnablespkg.UnansweredKeys(answers, bound)
182-
}
183-
if !listRunnablesTools {
184-
return listing, nil
80+
81+
if listRunnablesJSON {
82+
return json.NewEncoder(cmd.OutOrStdout()).Encode(entries)
18583
}
186-
tools := make([]runnableRow, 0, len(listing.Runnables))
187-
for i := range listing.Runnables {
188-
if listing.Runnables[i].Tool != nil {
189-
tools = append(tools, listing.Runnables[i])
190-
}
84+
85+
if len(entries) == 0 {
86+
fmt.Fprintf(cmd.OutOrStdout(), "No runnables in workspace <%s>\n", workspace.Name)
87+
return nil
19188
}
192-
listing.Runnables = tools
193-
return listing, nil
194-
}
19589

196-
func writeRunnables(cmd *cobra.Command, listing *runnableListing, env *environments.Environment) error {
19790
w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
198-
// The tool columns appear only when a caller asked about tools or about an
199-
// environment: the default table is the one every existing reader parses.
200-
tools := listRunnablesTools || env != nil
201-
header := "MODULE\tNAME\tVERSION\tAGENT\tFACILITIES\tTIMEOUT\tSOURCE"
202-
if tools {
203-
header += "\tTOOL\tEFFECT"
204-
}
205-
if env != nil {
206-
header += "\tKEY\tGRANT"
207-
}
208-
fmt.Fprintln(w, header)
209-
for index := range listing.Runnables {
210-
e := &listing.Runnables[index]
91+
fmt.Fprintln(w, "MODULE\tNAME\tVERSION\tAGENT\tFACILITIES\tTIMEOUT\tSOURCE")
92+
for index := range entries {
93+
e := &entries[index]
21194
source := e.Source
21295
if source == "" {
21396
source = "declared"
21497
}
215-
row := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\t%s",
98+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
21699
e.Module, e.Name, e.Version, e.Agent, strings.Join(e.Execution.Facilities, ","), e.Execution.Timeout, source)
217-
if tools {
218-
name, effect := "", ""
219-
if e.Tool != nil {
220-
name, effect = e.Tool.Name, e.Tool.Effect
221-
}
222-
row += "\t" + name + "\t" + effect
223-
}
224-
if env != nil {
225-
key, state := "", ""
226-
if e.Grant != nil {
227-
key, state = e.Grant.Key, e.Grant.State
228-
}
229-
row += "\t" + key + "\t" + state
230-
}
231-
fmt.Fprintln(w, row)
232-
}
233-
if err := w.Flush(); err != nil {
234-
return err
235-
}
236-
// A refused answer is printed under the table with its reason, because the
237-
// state alone ("invalid") does not say what to change.
238-
for index := range listing.Runnables {
239-
grant := listing.Runnables[index].Grant
240-
if grant != nil && grant.Reason != "" {
241-
fmt.Fprintf(cmd.OutOrStdout(), "\n%s: %s", grant.Key, grant.Reason)
242-
}
243-
}
244-
if len(listing.UnansweredKeys) > 0 {
245-
fmt.Fprintf(cmd.OutOrStdout(), "\n\nEnvironment %s answers keys no operation binds; generation refuses each by name:\n", env.Name)
246-
for _, key := range listing.UnansweredKeys {
247-
fmt.Fprintf(cmd.OutOrStdout(), " %s\n", key)
248-
}
249100
}
250-
return nil
101+
return w.Flush()
251102
}
252103

253104
func init() {
254105
RunnablesCmd.Flags().StringVar(&listRunnablesModule, "module", "", "Module to list runnables from")
255106
RunnablesCmd.Flags().BoolVar(&listRunnablesJSON, "json", false, "Emit machine-readable JSON")
256-
RunnablesCmd.Flags().StringVar(&listRunnablesEnv, "env", "", "Environment whose runnable-scope-selections to report as each operation's grant")
257-
RunnablesCmd.Flags().BoolVar(&listRunnablesTools, "tools", false, "List only the operations their owner exposes as a tool")
258107
}

0 commit comments

Comments
 (0)