Files
kandev/apps/backend/internal/agent/runtime/lifecycle/broker_reachability.go
T

127 lines
3.7 KiB
Go

package lifecycle
import (
"context"
"errors"
"fmt"
"strings"
"time"
agentctl "github.com/kandev/kandev/internal/agent/runtime/agentctl"
agentctltypes "github.com/kandev/kandev/internal/agentctl/types"
"github.com/kandev/kandev/internal/githubauth"
)
var ErrGitCredentialBrokerUnreachable = errors.New("git credential broker is unreachable from executor")
// ErrGitHubCredentialBrokerUnreachable remains an alias for callers which
// predate provider-neutral leases.
var ErrGitHubCredentialBrokerUnreachable = ErrGitCredentialBrokerUnreachable
const brokerReachabilityScript = `if ! command -v curl >/dev/null 2>&1; then
echo "curl is required to reach the Kandev Git credential broker" >&2
exit 69
fi
broker_status=$(curl -sS --connect-timeout 5 --max-time 10 -o /dev/null -w '%{http_code}' "$KANDEV_GITHUB_CREDENTIAL_BROKER_URL") || exit 69
if [ "$broker_status" != "204" ]; then
echo "Kandev Git credential broker readiness returned HTTP $broker_status" >&2
exit 69
fi`
type brokerCommandRunner func(context.Context, string, map[string]string) ([]byte, error)
func runBrokerReachabilityPreflight(
ctx context.Context,
env map[string]string,
run brokerCommandRunner,
) error {
if !hasManagedGitCredentialBrokerEnv(env) {
return nil
}
probeEnv := map[string]string{
githubauth.CredentialBrokerURLEnv: env[githubauth.CredentialBrokerURLEnv],
}
output, err := run(ctx, brokerReachabilityScript, probeEnv)
if err != nil {
return fmt.Errorf("%w: %s: %v", ErrGitCredentialBrokerUnreachable,
strings.TrimSpace(string(output)), err)
}
return nil
}
type brokerAgentctlProcessClient interface {
StartProcess(context.Context, agentctl.StartProcessRequest) (*agentctl.ProcessInfo, error)
GetProcess(context.Context, string, bool) (*agentctl.ProcessInfo, error)
}
func runBrokerReachabilityViaAgentctl(
ctx context.Context,
client brokerAgentctlProcessClient,
sessionID string,
env map[string]string,
) error {
return runBrokerReachabilityPreflight(ctx, env, func(
ctx context.Context,
command string,
probeEnv map[string]string,
) ([]byte, error) {
process, err := client.StartProcess(ctx, agentctl.StartProcessRequest{
SessionID: sessionID,
Kind: agentctltypes.ProcessKindCustom,
Command: command,
Env: probeEnv,
})
if err != nil {
return nil, err
}
return waitBrokerReachabilityProcess(ctx, client, process)
})
}
func waitBrokerReachabilityProcess(
ctx context.Context,
client brokerAgentctlProcessClient,
process *agentctl.ProcessInfo,
) ([]byte, error) {
if process == nil {
return nil, errors.New("agentctl returned no process")
}
deadline := time.NewTimer(15 * time.Second)
defer deadline.Stop()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
if output, done, err := brokerProcessResult(process); done {
return output, err
}
select {
case <-ctx.Done():
return nil, context.Cause(ctx)
case <-deadline.C:
return nil, errors.New("broker reachability process timed out")
case <-ticker.C:
var err error
process, err = client.GetProcess(ctx, process.ID, true)
if err != nil {
return nil, err
}
}
}
}
func brokerProcessResult(process *agentctl.ProcessInfo) ([]byte, bool, error) {
if process.Status != agentctltypes.ProcessStatusExited &&
process.Status != agentctltypes.ProcessStatusFailed &&
process.Status != agentctltypes.ProcessStatusStopped {
return nil, false, nil
}
var output strings.Builder
for _, chunk := range process.Output {
output.WriteString(chunk.Data)
}
if process.ExitCode == nil || *process.ExitCode != 0 || process.Status != agentctltypes.ProcessStatusExited {
return []byte(output.String()), true, fmt.Errorf("probe process status %s", process.Status)
}
return []byte(output.String()), true, nil
}