Skip to content
Open
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
15 changes: 10 additions & 5 deletions cli/cmd/bootstrap_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) {
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.ProjectTTL, "project-ttl", "2h", "Time to live for the GCP project. Cleanup workflows will remove it afterwards. (default: 2 hours)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.BillingAccount, "billing-account", "", "GCP Billing Account ID (required)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.BaseDomain, "base-domain", "", "Base domain for Codesphere (required)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GithubAppClientID, "github-app-client-id", "", "Github App Client ID (required)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GithubAppClientSecret, "github-app-client-secret", "", "Github App Client Secret (required)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubPAT, "github-pat", "", "GitHub Personal Access Token to use for direct image access. Scope required: package read (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubAppName, "github-app-name", "", "Github App Name (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubAppClientID, "github-app-client-id", "", "GitHub App Client ID (required)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubAppClientSecret, "github-app-client-secret", "", "GitHub App Client Secret (required)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubPAT, "github-pat", "", "GitHub Personal Access Token used for direct image access and fetching team SSH keys. Required when using --github-team-org/--github-team-slug. Required scopes: read:packages, read:org.")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubAppName, "github-app-name", "", "GitHub App Name (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubTeamOrg, "github-team-org", "", "GitHub organization used to fetch team SSH keys (optional, used with --github-team-slug). Requires --github-pat with at least the read:org scope.")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubTeamSlug, "github-team-slug", "", "GitHub team slug used to fetch team SSH keys (optional, used with --github-team-org). Requires --github-pat with at least the read:org scope.")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.SecretsDir, "secrets-dir", "/etc/codesphere/secrets", "Directory for secrets (default: /etc/codesphere/secrets)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.FolderID, "folder-id", "", "GCP Folder ID (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.SSHPublicKeyPath, "ssh-public-key-path", "~/.ssh/id_rsa.pub", "SSH Public Key Path (default: ~/.ssh/id_rsa.pub)")
Expand Down Expand Up @@ -110,8 +112,10 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error {
gcpClient := gcp.NewGCPClient(ctx, stlog, os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
fw := util.NewFilesystemWriter()
portalClient := portal.NewPortalClient()
githubClient := gcp.NewGitHubClient(ctx, c.CodesphereEnv.GitHubPAT)

bs, err := gcp.NewGCPBootstrapper(ctx,
bs, err := gcp.NewGCPBootstrapper(
ctx,
c.Env,
stlog,
c.CodesphereEnv,
Expand All @@ -121,6 +125,7 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error {
node.NewSSHNodeClient(c.SSHQuiet),
portalClient,
util.NewTime(),
githubClient,
)
if err != nil {
return err
Expand Down
10 changes: 6 additions & 4 deletions docs/oms_beta_bootstrap-gcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ oms beta bootstrap-gcp [flags]
--experiments stringArray Experiments to enable in Codesphere installation (optional) (default [managed-services,vcluster,custom-service-image,ms-in-ls,secret-management,sub-path-mount])
--feature-flags stringArray Feature flags to enable in Codesphere installation (optional)
--folder-id string GCP Folder ID (optional)
--github-app-client-id string Github App Client ID (required)
--github-app-client-secret string Github App Client Secret (required)
--github-app-name string Github App Name (optional)
--github-pat string GitHub Personal Access Token to use for direct image access. Scope required: package read (optional)
--github-app-client-id string GitHub App Client ID (required)
--github-app-client-secret string GitHub App Client Secret (required)
--github-app-name string GitHub App Name (optional)
--github-pat string GitHub Personal Access Token used for direct image access and fetching team SSH keys. Required when using --github-team-org/--github-team-slug. Required scopes: read:packages, read:org.
--github-team-org string GitHub organization used to fetch team SSH keys (optional, used with --github-team-slug). Requires --github-pat with at least the read:org scope.
--github-team-slug string GitHub team slug used to fetch team SSH keys (optional, used with --github-team-org). Requires --github-pat with at least the read:org scope.
-h, --help help for bootstrap-gcp
--install-config string Path to install config file (optional) (default "config.yaml")
--install-hash string Codesphere package hash to install (default: none)
Expand Down
136 changes: 114 additions & 22 deletions internal/bootstrap/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/codesphere-cloud/oms/internal/installer/node"
"github.com/codesphere-cloud/oms/internal/portal"
"github.com/codesphere-cloud/oms/internal/util"
"github.com/google/go-github/v74/github"
"github.com/lithammer/shortuuid"
"google.golang.org/api/dns/v1"
"google.golang.org/api/googleapi"
Expand Down Expand Up @@ -122,6 +123,7 @@ type GCPBootstrapper struct {
// SSH command runner
NodeClient node.NodeClient
PortalClient portal.Portal
GitHubClient GitHubClient
}

type CodesphereEnvironment struct {
Expand All @@ -146,6 +148,8 @@ type CodesphereEnvironment struct {
RegistryType RegistryType `json:"registry_type"`
GitHubPAT string `json:"-"`
GitHubAppName string `json:"-"`
GitHubTeamOrg string `json:"github_team_org"`
GitHubTeamSlug string `json:"github_team_slug"`
RegistryUser string `json:"-"`
Experiments []string `json:"experiments"`
FeatureFlags []string `json:"feature_flags"`
Expand All @@ -166,8 +170,8 @@ type CodesphereEnvironment struct {
ProjectDisplayName string `json:"project_display_name"`
BillingAccount string `json:"billing_account"`
BaseDomain string `json:"base_domain"`
GithubAppClientID string `json:"-"`
GithubAppClientSecret string `json:"-"`
GitHubAppClientID string `json:"-"`
GitHubAppClientSecret string `json:"-"`
SecretsDir string `json:"secrets_dir"`
FolderID string `json:"folder_id"`
SSHPublicKeyPath string `json:"-"`
Expand All @@ -190,6 +194,7 @@ func NewGCPBootstrapper(
sshRunner node.NodeClient,
portalClient portal.Portal,
time util.Time,
gitHubClient GitHubClient,
) (*GCPBootstrapper, error) {
return &GCPBootstrapper{
ctx: ctx,
Expand All @@ -201,6 +206,7 @@ func NewGCPBootstrapper(
NodeClient: sshRunner,
PortalClient: portalClient,
Time: time,
GitHubClient: gitHubClient,
}, nil
}

Expand Down Expand Up @@ -375,7 +381,7 @@ func (b *GCPBootstrapper) ValidateInput() error {
return err
}

return b.validateGithubParams()
return b.validateGitHubParams()
}

// validateInstallVersion checks if the specified install version exists and contains the required installer artifact
Expand Down Expand Up @@ -417,11 +423,20 @@ func (b *GCPBootstrapper) validateInstallVersion() error {
return fmt.Errorf("specified package does not contain required installer artifact %s. Existing artifacts: %s", requiredFilename, strings.Join(filenames, ", "))
}

// validateGithubParams checks if the GitHub credentials are fully specified if GitHub registry is selected
func (b *GCPBootstrapper) validateGithubParams() error {
ghParams := []string{b.Env.GitHubAppName, b.Env.GithubAppClientID, b.Env.GithubAppClientSecret}
if slices.Contains(ghParams, "") && strings.Join(ghParams, "") != "" {
return fmt.Errorf("GitHub app credentials are not fully specified (all or none of GitHubAppName, GithubAppClientID, GithubAppClientSecret must be set)")
// validateGitHubParams checks if the GitHub credentials are fully specified if GitHub registry is selected
func (b *GCPBootstrapper) validateGitHubParams() error {
if b.Env.GitHubTeamSlug != "" && b.Env.GitHubTeamOrg != "" && b.Env.GitHubPAT == "" {
return fmt.Errorf("GitHub PAT is required to extract public keys of GitHub team members")
}

ghTeamParams := []string{b.Env.GitHubTeamSlug, b.Env.GitHubTeamOrg}
if slices.Contains(ghTeamParams, "") && strings.Join(ghTeamParams, "") != "" {
return fmt.Errorf("GitHub team parameters are not fully specified (all or none of GitHubTeamSlug, GitHubTeamOrg must be set)")
}

ghAppParams := []string{b.Env.GitHubAppName, b.Env.GitHubAppClientID, b.Env.GitHubAppClientSecret}
if slices.Contains(ghAppParams, "") && strings.Join(ghAppParams, "") != "" {
return fmt.Errorf("GitHub app credentials are not fully specified (all or none of GitHubAppName, GitHubAppClientID, GitHubAppClientSecret must be set)")
}

return nil
Expand Down Expand Up @@ -757,14 +772,30 @@ func (b *GCPBootstrapper) EnsureComputeInstances() error {
subnetwork := fmt.Sprintf("projects/%s/regions/%s/subnetworks/%s-%s-subnet", projectID, region, projectID, region)
diskType := fmt.Sprintf("projects/%s/zones/%s/diskTypes/pd-ssd", projectID, zone)

// Create VMs in parallel
wg := sync.WaitGroup{}
errCh := make(chan error, len(vmDefs))
resultCh := make(chan vmResult, len(vmDefs))
rootDiskSize := int64(200)
if b.Env.RegistryType == RegistryTypeGitHub {
rootDiskSize = 50
}
sshKeys := ""
var err error
if b.Env.GitHubPAT != "" && b.Env.GitHubTeamOrg != "" && b.Env.GitHubTeamSlug != "" {
sshKeys, err = b.getSSHKeysFromGitHubTeam()
if err != nil {
return fmt.Errorf("failed to get SSH keys from GitHub team: %w", err)
}
}

pubKey, err := b.readSSHKey(b.Env.SSHPublicKeyPath)
if err != nil {
return err
}

sshKeys += fmt.Sprintf("root:%s\nubuntu:%s", pubKey+"root", pubKey+"ubuntu")

// Create VMs in parallel
wg := sync.WaitGroup{}
errCh := make(chan error, len(vmDefs))
resultCh := make(chan vmResult, len(vmDefs))
for _, vm := range vmDefs {
wg.Add(1)
go func(vm VMDef) {
Expand Down Expand Up @@ -793,12 +824,6 @@ func (b *GCPBootstrapper) EnsureComputeInstances() error {
})
}

pubKey, err := b.readSSHKey(b.Env.SSHPublicKeyPath)
if err != nil {
errCh <- fmt.Errorf("failed to read SSH public key: %w", err)
return
}

serviceAccount := fmt.Sprintf("cloud-controller@%s.iam.gserviceaccount.com", projectID)
instance := &computepb.Instance{
Name: protoString(vm.Name),
Expand Down Expand Up @@ -826,7 +851,7 @@ func (b *GCPBootstrapper) EnsureComputeInstances() error {
Items: []*computepb.Items{
{
Key: protoString("ssh-keys"),
Value: protoString(fmt.Sprintf("root:%s\nubuntu:%s", pubKey+"root", pubKey+"ubuntu")),
Value: protoString(sshKeys),
},
},
},
Expand Down Expand Up @@ -918,6 +943,71 @@ func (b *GCPBootstrapper) EnsureComputeInstances() error {
return nil
}

// getSSHKeysFromGitHubTeam fetches the public SSH keys of all members of the specified GitHub team and formats them for inclusion in instance metadata.
func (b *GCPBootstrapper) getSSHKeysFromGitHubTeam() (string, error) {
if b.Env.GitHubTeamSlug == "" || b.Env.GitHubTeamOrg == "" || b.Env.GitHubPAT == "" {
return "", fmt.Errorf("GitHub team slug, org, and PAT must be specified to fetch SSH keys from GitHub team")
}
allKeys := ""

allMembers, err := b.listAllGitHubTeamMembers(b.ctx, b.Env, b.Env.GitHubTeamOrg, b.Env.GitHubTeamSlug)
if err != nil {
return "", fmt.Errorf("failed to list GitHub team members: %w", err)
}

b.stlog.Logf("Found %d members in team '%s'", len(allMembers), b.Env.GitHubTeamSlug)

for _, user := range allMembers {
username := user.GetLogin()
keys, err := b.GitHubClient.ListUserKeys(b.ctx, username)
if err != nil {
b.stlog.Logf("Could not fetch keys for %s: %v", username, err)
continue
}

for _, key := range keys {
allKeys += fmt.Sprintf("root:%s %sroot\nubuntu:%s %subuntu\n", key.GetKey(), username, key.GetKey(), username)
}
}

return allKeys, nil
}

// listAllGitHubTeamMembers retrieves all members of the specified GitHub team, handling pagination to ensure all members are fetched.
func (b *GCPBootstrapper) listAllGitHubTeamMembers(ctx context.Context, env *CodesphereEnvironment, org string, teamSlug string) ([]*github.User, error) {
perPage := 100
page := 1
var allMembers []*github.User

for {
opts := &github.TeamListTeamMembersOptions{
ListOptions: github.ListOptions{
Page: page,
PerPage: perPage,
},
}

members, err := b.GitHubClient.ListTeamMembersBySlug(b.ctx, b.Env.GitHubTeamOrg, b.Env.GitHubTeamSlug, opts)
if err != nil {
return nil, fmt.Errorf("failed to fetch team members from GitHub: %w", err)
}

if len(members) == 0 {
break
}

allMembers = append(allMembers, members...)

if len(members) < perPage {
break
}

page++
}

return allMembers, nil
}

// EnsureGatewayIPAddresses reserves 2 static external IP addresses for the ingress
// controllers of the cluster.
func (b *GCPBootstrapper) EnsureGatewayIPAddresses() error {
Expand All @@ -930,6 +1020,7 @@ func (b *GCPBootstrapper) EnsureGatewayIPAddresses() error {
if err != nil {
return fmt.Errorf("failed to ensure public gateway IP: %w", err)
}

return nil
}

Expand Down Expand Up @@ -961,6 +1052,7 @@ func (b *GCPBootstrapper) EnsureExternalIP(name string) (string, error) {
if err == nil && address != nil {
return address.GetAddress(), nil
}

return "", fmt.Errorf("failed to get address %s after creation", name)
}

Expand Down Expand Up @@ -1332,7 +1424,7 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error {
}

b.Env.InstallConfig.Codesphere.GitProviders = &files.GitProvidersConfig{}
if b.Env.GitHubAppName != "" && b.Env.GithubAppClientID != "" && b.Env.GithubAppClientSecret != "" {
if b.Env.GitHubAppName != "" && b.Env.GitHubAppClientID != "" && b.Env.GitHubAppClientSecret != "" {
b.Env.InstallConfig.Codesphere.GitProviders.GitHub = &files.GitProviderConfig{
Enabled: true,
URL: "https://github.com",
Expand All @@ -1347,8 +1439,8 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error {
RedirectURI: "https://cs." + b.Env.BaseDomain + "/ide/auth/github/callback",
InstallationURI: "https://github.com/apps/" + b.Env.GitHubAppName + "/installations/new",

ClientID: b.Env.GithubAppClientID,
ClientSecret: b.Env.GithubAppClientSecret,
ClientID: b.Env.GitHubAppClientID,
ClientSecret: b.Env.GitHubAppClientSecret,
},
}
}
Expand Down
Loading
Loading