Skip to content
Merged
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
28 changes: 23 additions & 5 deletions cmd/configure_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"strings"
"syscall"
"time"
"unicode"

"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
Expand Down Expand Up @@ -751,8 +752,6 @@ func gcpStepSelectProject(ctx context.Context, reader *bufio.Reader) (string, er
// SDK. It fails loud on any SDK error (no CLI fallback).
func gcpStepCreateServiceAccount(ctx context.Context, reader *bufio.Reader, projectID string) (string, error) {
saName := "cudly-service-account"
saEmail := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", saName, projectID)

fmt.Println()
fmt.Println("Step 3: Create Service Account")
fmt.Println("------------------------------")
Expand All @@ -770,14 +769,33 @@ func gcpStepCreateServiceAccount(ctx context.Context, reader *bufio.Reader, proj
if createErr != nil {
return "", createErr
}
saEmail = email
fmt.Printf("Service account created: %s\n", saEmail)
fmt.Printf("Service account created: %s\n", email)
return email, nil
case "s", "skip":
fmt.Println("Skipping Create Service Account")
return promptExistingGCPServiceAccount(reader)
default:
fmt.Printf("Unknown option, skipping\n")
return promptExistingGCPServiceAccount(reader)
}
}

// promptExistingGCPServiceAccount asks for the email of an existing service
// account when creation is skipped, so later steps never act on a principal
// that was neither created nor supplied by the operator.
func promptExistingGCPServiceAccount(reader *bufio.Reader) (string, error) {
email, err := readRequiredInputLine(reader, "Skipped. Enter the existing service account email (name@project.iam.gserviceaccount.com): ", "service-account email")
if err != nil {
return "", err
}
email = strings.ToLower(email)
local, domain, ok := strings.Cut(email, "@")
label, hasSuffix := strings.CutSuffix(domain, ".gserviceaccount.com")
if !ok || local == "" || !hasSuffix || label == "" || strings.Contains(domain, "@") ||
strings.IndexFunc(email, func(r rune) bool { return unicode.IsSpace(r) || unicode.IsControl(r) }) >= 0 {
return "", fmt.Errorf("invalid service account email %q: expected a name@<project>.gserviceaccount.com address", email)
}
return saEmail, nil
return email, nil
}

// gcpCustomRole is a single-permission custom role the wizard provisions.
Expand Down
2 changes: 1 addition & 1 deletion cmd/configure_gcp_iam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func runGCPIAMFixture(t *testing.T, scenario string) {
stdin, writeErr := os.Create(filepath.Join(dir, "stdin"))
require.NoError(t, writeErr)
defer stdin.Close()
_, writeErr = io.WriteString(stdin, "s\ns\ns\nfixture-project\ns\nr\n")
_, writeErr = io.WriteString(stdin, "s\ns\ns\nfixture-project\ns\ncudly-service-account@fixture-project.iam.gserviceaccount.com\nr\n")
require.NoError(t, writeErr)
_, writeErr = stdin.Seek(0, io.SeekStart)
require.NoError(t, writeErr)
Expand Down
2 changes: 1 addition & 1 deletion cmd/configure_gcp_signal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func startGCPInterruptChild(t *testing.T, phase string) (*exec.Cmd, string, io.R
child.Stderr = stderr
require.NoError(t, child.Start())
t.Cleanup(func() { _ = child.Process.Kill() })
input := "s\ns\ns\nfixture-project\ns\ns\n"
input := "s\ns\ns\nfixture-project\ns\ncudly-service-account@fixture-project.iam.gserviceaccount.com\ns\n"
if phase != "prompt" {
input += "r\n"
}
Expand Down
40 changes: 40 additions & 0 deletions cmd/configure_gcp_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package main

import (
"bufio"
"context"
"encoding/base64"
"errors"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -398,3 +400,41 @@ func TestGcloudDeleteKeyCommand(t *testing.T) {
"gcloud iam service-accounts keys delete abc123 --iam-account=sa@proj.iam.gserviceaccount.com",
gcloudDeleteKeyCommand(testMintedKeyName))
}

// --- gcpStepCreateServiceAccount ---

func TestGCPStepCreateServiceAccount_SkipUsesOperatorEmail(t *testing.T) {
const existing = "existing-sa@my-proj.iam.gserviceaccount.com"
reader := bufio.NewReader(strings.NewReader("s\n " + existing + " \n"))

email, err := gcpStepCreateServiceAccount(context.Background(), reader, "my-proj")

require.NoError(t, err)
assert.Equal(t, existing, email)
assert.NotEqual(t, "cudly-service-account@my-proj.iam.gserviceaccount.com", email)
}

func TestGCPStepCreateServiceAccount_UnknownOptionPromptsForEmail(t *testing.T) {
const existing = "other@my-proj.iam.gserviceaccount.com"
reader := bufio.NewReader(strings.NewReader("x\n" + existing + "\n"))

email, err := gcpStepCreateServiceAccount(context.Background(), reader, "my-proj")

require.NoError(t, err)
assert.Equal(t, existing, email)
}

func TestGCPStepCreateServiceAccount_SkipRejectsInvalidEmail(t *testing.T) {
for _, in := range []string{"s\n\n", "s\nnot-an-email\n", "s\nuser@example.com\n", "s\n@x.gserviceaccount.com\n", "s\na@b@c.gserviceaccount.com\n", "s\na@.gserviceaccount.com\n", "s\n ", "s\na\x00@x.gserviceaccount.com\n", "s\n"} {
email, err := gcpStepCreateServiceAccount(context.Background(), bufio.NewReader(strings.NewReader(in)), "my-proj")
require.Error(t, err, "input %q", in)
assert.Empty(t, email)
}
}

func TestGCPStepCreateServiceAccount_SkipAcceptsUppercaseEmail(t *testing.T) {
const existing = "Existing-SA@My-Proj.IAM.GServiceAccount.com"
email, err := gcpStepCreateServiceAccount(context.Background(), bufio.NewReader(strings.NewReader("s\n"+existing+"\n")), "my-proj")
require.NoError(t, err)
assert.Equal(t, strings.ToLower(existing), email)
}
2 changes: 2 additions & 0 deletions docs/cli/cloud-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ When `--skip-setup` is not set, the command runs an interactive guided flow:
5. **Grant IAM roles via SDK** - creates or validates the project custom role `cudlyCommitmentPurchaser`, then grants it and `roles/compute.viewer` to the Service Account.
6. **`gcloud iam service-accounts keys create ~/cudly-gcp-key.json`** - downloads a JSON key to your home directory.

Choosing Skip at the service-account step prompts for the email of an existing service account, which is used for the role grant and key steps.

After the guided steps (or with `--skip-setup --credentials-file <path>`), the command reads and validates the JSON file and writes it to the `<stack-name>-GCPCredentials` secret.

### Non-interactive usage
Expand Down
Loading