Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 100 additions & 85 deletions app/cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,101 @@ func handleOrganizationExecutionError(executed *cobra.Command, err error) error
return err
}

// ensureActionOpts builds the control-plane connection and the shared
// ActionOpts from the resolved configuration: the auth token, an optional
// federated token, the gRPC options, the current-organization lookup, and the
// interactive-token warning. It is the control-plane half of the root
// PersistentPreRunE, factored out so a command annotated skipActionOptsInit can
// skip it entirely and, when it does need the control plane, call this lazily
// instead. A trace command in LOCAL storage mode never calls it, so it makes no
// control-plane request (R-005). It sets the package-level ActionOpts.
func ensureActionOpts(cmd *cobra.Command) error {
if apiInsecure() {
logger.Debug().Msg("API contacted in insecure mode")
}

authToken, isUserToken, err := loadAuthToken(cmd)
if err != nil {
return err
}
authTokenIsUser = isUserToken

// If the auth token is not set and the command supports federated auth, we try to discover the runner and use the federated token for the runner if available
if authToken == "" && cmdSupportsFederatedAuth(cmd) {
r := crafter.DiscoverRunner(authToken, logger)
if r.IsAuthenticated() && r.FederatedToken() != "" {
logger.Debug().Str("runner", r.ID().String()).Msg("using federated auth token")
authToken = r.FederatedToken()
// reset isUserToken to false because we are using a federated token and we don't want to ask for confirmation
isUserToken = false
}
}

var opts = []grpcconn.Option{
grpcconn.WithInsecure(apiInsecure()),
grpcconn.WithCLIVersion(fullVersion()),
}

if maxRecv := apiMaxRecvMsgSize(); maxRecv > 0 {
opts = append(opts, grpcconn.WithMaxRecvMsgSize(maxRecv))
}

if caValue := viper.GetString(confOptions.controlplaneCA.viperKey); caValue != "" {
// Check if the value is a file path, if it is we read the content and encode it to base64, if not we assume it's the content already
if _, err := os.Stat(caValue); err == nil {
opts = append(opts, grpcconn.WithCAFile(caValue))
} else {
opts = append(opts, grpcconn.WithCAContent(caValue))
}
}

controlplaneURL := viper.GetString(confOptions.controlplaneAPI.viperKey)

// If no organization is selected, load it from the server and save it.
orgName := effectiveOrganization(cmd)
if orgName == "" {
conn, err := grpcconn.New(controlplaneURL, authToken, opts...)
if err != nil {
return err
}

currentContext, err := action.NewConfigCurrentContext(newActionOpts(logger, conn, authToken)).Run()
if err == nil && currentContext.CurrentMembership != nil {
if err := setLocalOrganization(currentContext.CurrentMembership.Org.Name); err != nil {
return fmt.Errorf("writing config file: %w", err)
}
}
}

// Reload the connection now that we have the organization name.
orgName = effectiveOrganization(cmd)
if orgName != "" {
opts = append(opts, grpcconn.WithOrgName(orgName))
}

// Warn users when the session is interactive, and the operation is supposed to use an API token instead
if shouldAskForConfirmation(cmd) && authToken != "" && isUserToken && !flagYes {
logger.Warn().Msg("User-attended mode detected. This is intended for local testing only. For CI/CD or automated workflows, please use an API token.")
if !confirmationPrompt(fmt.Sprintf("This command will run against the organization %q", orgName)) {
return errors.New("command canceled by user")
}
}

conn, err := grpcconn.New(controlplaneURL, authToken, opts...)
if err != nil {
return err
}

ActionOpts = newActionOpts(logger, conn, authToken)

// Add custom builtins to rego engine
if err = builtins.RegisterDiscoverBuiltin(conn); err != nil {
return fmt.Errorf("failed to register discover builtin: %w", err)
}

return nil
}

func NewRootCmd(l zerolog.Logger) *cobra.Command {
// Seed the root logger here and not only in PersistentPreRunE: cobra
// rejects unknown subcommands and invalid flags before that hook runs, and
Expand Down Expand Up @@ -175,95 +270,15 @@ Command reference: ` + cliReferenceURL,
logger.Debug().Str("path", viper.ConfigFileUsed()).Msg("using config file")

// Commands annotated with skipActionOptsInit don't need ActionOpts initialization
// These are local-only commands that don't interact with the control plane
// These are local-only commands that don't interact with the control plane.
// The trace pre-push hook and `trace init` carry the annotation and build
// ActionOpts lazily with ensureActionOpts, only in their control-plane
// branch, so a LOCAL-mode repository makes no control-plane call at all.
if cmd.Annotations[skipActionOptsInit] == trueString {
return nil
}

if apiInsecure() {
logger.Debug().Msg("API contacted in insecure mode")
}

authToken, isUserToken, err := loadAuthToken(cmd)
if err != nil {
return err
}
authTokenIsUser = isUserToken

// If the auth token is not set and the command supports federated auth, we try to discover the runner and use the federated token for the runner if available
if authToken == "" && cmdSupportsFederatedAuth(cmd) {
r := crafter.DiscoverRunner(authToken, logger)
if r.IsAuthenticated() && r.FederatedToken() != "" {
logger.Debug().Str("runner", r.ID().String()).Msg("using federated auth token")
authToken = r.FederatedToken()
// reset isUserToken to false because we are using a federated token and we don't want to ask for confirmation
isUserToken = false
}
}

var opts = []grpcconn.Option{
grpcconn.WithInsecure(apiInsecure()),
grpcconn.WithCLIVersion(fullVersion()),
}

if maxRecv := apiMaxRecvMsgSize(); maxRecv > 0 {
opts = append(opts, grpcconn.WithMaxRecvMsgSize(maxRecv))
}

if caValue := viper.GetString(confOptions.controlplaneCA.viperKey); caValue != "" {
// Check if the value is a file path, if it is we read the content and encode it to base64, if not we assume it's the content already
if _, err := os.Stat(caValue); err == nil {
opts = append(opts, grpcconn.WithCAFile(caValue))
} else {
opts = append(opts, grpcconn.WithCAContent(caValue))
}
}

controlplaneURL := viper.GetString(confOptions.controlplaneAPI.viperKey)

// If no organization is selected, load it from the server and save it.
orgName := effectiveOrganization(cmd)
if orgName == "" {
conn, err := grpcconn.New(controlplaneURL, authToken, opts...)
if err != nil {
return err
}

currentContext, err := action.NewConfigCurrentContext(newActionOpts(logger, conn, authToken)).Run()
if err == nil && currentContext.CurrentMembership != nil {
if err := setLocalOrganization(currentContext.CurrentMembership.Org.Name); err != nil {
return fmt.Errorf("writing config file: %w", err)
}
}
}

// Reload the connection now that we have the organization name.
orgName = effectiveOrganization(cmd)
if orgName != "" {
opts = append(opts, grpcconn.WithOrgName(orgName))
}

// Warn users when the session is interactive, and the operation is supposed to use an API token instead
if shouldAskForConfirmation(cmd) && authToken != "" && isUserToken && !flagYes {
logger.Warn().Msg("User-attended mode detected. This is intended for local testing only. For CI/CD or automated workflows, please use an API token.")
if !confirmationPrompt(fmt.Sprintf("This command will run against the organization %q", orgName)) {
return errors.New("command canceled by user")
}
}

conn, err := grpcconn.New(controlplaneURL, authToken, opts...)
if err != nil {
return err
}

ActionOpts = newActionOpts(logger, conn, authToken)

// Add custom builtins to rego engine
if err = builtins.RegisterDiscoverBuiltin(conn); err != nil {
return fmt.Errorf("failed to register discover builtin: %w", err)
}

return nil
return ensureActionOpts(cmd)
},
PersistentPostRunE: func(_ *cobra.Command, _ []string) error {
if ActionOpts != nil {
Expand Down
25 changes: 24 additions & 1 deletion app/cli/cmd/trace_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func applyPrePushPolicy(executed *cobra.Command, err error) error {
return err
}

return action.PrePushFailure(err, config.LoadRequireTraceFromYML("."), logger)
return action.PrePushFailure(err, config.LoadTraceRequiredFromYML("."), logger)
}

func newTraceHookGitPrePushCmd() *cobra.Command {
Expand All @@ -121,12 +121,35 @@ func newTraceHookGitPrePushCmd() *cobra.Command {
Short: "Handle the pre-push git hook",
// Any error of this command, setup included, goes through
// action.PrePushFailure in Execute.
//
// skipActionOptsInit keeps root from building the control-plane
// connection (and probing the current org) up front: a LOCAL-mode
// repository must reach no control plane, so the connection is built
// lazily below, only when the storage mode is CONTROL_PLANE.
Annotations: map[string]string{
prePushHookAnnotation: trueString,
skipActionOptsInit: trueString,
},
RunE: func(cmd *cobra.Command, _ []string) error {
InitHookLogger()

// The hook runs at the repository root, so "." resolves the nearest
// .chainloop.yml the same way the push identity does.
mode, err := config.LoadTraceStorageFromYML(".")
if err != nil {
return err
}

if mode == config.TraceStorageLocal {
return action.HandlePrePushHookLocal(cmd.Context(), logger)
}

// Control plane: build ActionOpts lazily, since this command is
// skipActionOptsInit and root did not.
if err := ensureActionOpts(cmd); err != nil {
return err
}

return action.HandlePrePushHook(cmd.Context(), logger, action.RunTracePushOpts{
ActionOpts: ActionOpts,
CLIVersion: Version,
Expand Down
Loading
Loading