Skip to content
Open
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
81 changes: 74 additions & 7 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -1938,9 +1938,14 @@ func getClientSettings(ctx context.Context) (map[string]string, error) {
// picker populated from server-reported available_regions when --region is
// unset and the CLI is interactive. In non-interactive mode an unset --region
// is an error so invocations fail loudly instead of silently defaulting.
//
// Regions the server flags in residency_warning_regions are annotated in the
// picker and confirmed before use; see confirmRegionResidency.
func resolveRegion(cmd *cli.Command, settingsMap map[string]string, title string) (string, error) {
warnRegions := splitSetting(settingsMap["residency_warning_regions"])

if region := cmd.String("region"); region != "" {
return region, nil
return region, confirmRegionResidency(cmd, region, settingsMap["project_data_region"], warnRegions)
}

availableRegionsStr, ok := settingsMap["available_regions"]
Expand All @@ -1950,30 +1955,93 @@ func resolveRegion(cmd *cli.Command, settingsMap map[string]string, title string
return "us-east", nil
}

regionOptions := strings.Split(availableRegionsStr, ",")
for i, r := range regionOptions {
regionOptions[i] = strings.TrimSpace(r)
}
regionOptions := splitSetting(availableRegionsStr)
slices.Sort(regionOptions)
slices.Reverse(regionOptions)

if SkipPrompts(cmd) {
return "", fmt.Errorf("non-interactive mode: --region flag must be specified, available regions: %v", regionOptions)
}

options := make([]huh.Option[string], 0, len(regionOptions))
for _, r := range regionOptions {
label := r
if slices.Contains(warnRegions, r) {
label = r + " " + util.Dimmed("⚠︎ GDPR compliance required")
}
options = append(options, huh.NewOption(label, r))
}

var region string
if err := huh.NewSelect[string]().
Title(title).
Options(huh.NewOptions(regionOptions...)...).
Options(options...).
Value(&region).
WithTheme(util.Theme).
Run(); err != nil {
return "", err
}
if err := confirmRegionResidency(cmd, region, settingsMap["project_data_region"], warnRegions); err != nil {
return "", err
}
out.Statusf("Using region [%s]", util.Accented(region))
return region, nil
}

// confirmRegionResidency asks the user to confirm deploying into a region the
// server flagged as a data-residency risk for this project — an EU region while
// the project stores its data elsewhere, meaning an agent that may serve EU end
// users records their data outside the EU.
//
// Deploying there is permitted, so this only prompts; in non-interactive mode it
// warns and proceeds rather than failing, since the region was named explicitly
// and blocking would break existing automation. An older server sends no warning
// regions, in which case nothing here fires.
func confirmRegionResidency(cmd *cli.Command, region, dataRegion string, warnRegions []string) error {
if !slices.Contains(warnRegions, region) {
return nil
}

detail := fmt.Sprintf(
"Data residency warning: This agent will run in [%s], but your project observability region is [%s]. Its recordings, transcripts, and traces will be stored outside the EU, which may breach GDPR. To keep this data in-region, create a new project with with an EU observability region.",
region,
dataRegion,
)
if SkipPrompts(cmd) {
out.Warnf("Deploying to [%s]. %s", region, detail)
return nil
}

confirmed := false
if err := huh.NewForm(huh.NewGroup(util.Confirm().
Title(fmt.Sprintf("Are you sure you want to deploy to [%s]?", region)).
Description(detail).
Affirmative("Deploy").
Negative("Cancel").
Value(&confirmed))).
WithTheme(util.Theme).
Run(); err != nil {
return err
}
if !confirmed {
return fmt.Errorf("deployment cancelled")
}
return nil
}

// splitSetting parses a comma-separated client setting into trimmed values,
// returning nil for an absent or empty setting.
func splitSetting(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
parts := strings.Split(value, ",")
for i, p := range parts {
parts[i] = strings.TrimSpace(p)
}
return parts
}

func requireConfig(workingDir, tomlFilename string) (bool, error) {
if lkConfig != nil {
return true, nil
Expand All @@ -1998,7 +2066,6 @@ func generateAgentDockerfile(ctx context.Context, cmd *cli.Command) error {
if err != nil {
return err
}

projectType, err := agentfs.DetectProjectType(os.DirFS(workingDir))
if err != nil {
return noAgentError()
Expand Down
Loading