Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code for multiproject GCE Client creation #2725

Merged
merged 1 commit into from
Nov 26, 2024
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
36 changes: 25 additions & 11 deletions cmd/glbc/app/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,30 +78,44 @@ func NewKubeConfig(logger klog.Logger) (*rest.Config, error) {
return config, nil
}

// GCEConfString returns the default GCE cluster config file content as a string.
func GCEConfString(logger klog.Logger) (string, error) {
if flags.F.ConfigFilePath == "" {
return "", fmt.Errorf("config file path is not specified")
}

logger.Info("Reading config from the specified path", "path", flags.F.ConfigFilePath)
configBytes, err := os.ReadFile(flags.F.ConfigFilePath)
if err != nil {
return "", fmt.Errorf("failed to read config file: %v", err)
}

configString := string(configBytes)
logger.V(4).Info("Cloudprovider config file", "config", configString)

return configString, nil
}

// NewGCEClient returns a client to the GCE environment. This will block until
// a valid configuration file can be read.
func NewGCEClient(logger klog.Logger) *gce.Cloud {
var configReader func() io.Reader
if flags.F.ConfigFilePath != "" {
logger.Info("Reading config from the specified path", "path", flags.F.ConfigFilePath)
config, err := os.Open(flags.F.ConfigFilePath)
if err != nil {
klog.Fatalf("%v", err)
}
defer config.Close()

allConfig, err := io.ReadAll(config)
allConfig, err := GCEConfString(logger)
if err != nil {
klog.Fatalf("Error while reading config (%q): %v", flags.F.ConfigFilePath, err)
klog.Fatalf("Error getting default cluster GCE config: %v", err)
}
logger.V(4).Info("Cloudprovider config file", "config", string(allConfig))

configReader = generateConfigReaderFunc(allConfig)
configReader = generateConfigReaderFunc([]byte(allConfig))
} else {
logger.V(2).Info("No cloudprovider config file provided, using default values")
configReader = func() io.Reader { return nil }
}

return GCEClientForConfigReader(configReader, logger)
}

func GCEClientForConfigReader(configReader func() io.Reader, logger klog.Logger) *gce.Cloud {
// Creating the cloud interface involves resolving the metadata server to get
// an oauth token. If this fails, the token provider assumes it's not on GCE.
// No errors are thrown. So we need to keep retrying till it works because
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.8
require (
github.com/GoogleCloudPlatform/gke-networking-api v0.1.2-0.20240909212819-4b1bab7c69ea
github.com/GoogleCloudPlatform/k8s-cloud-provider v1.32.0
github.com/go-ini/ini v1.67.0
github.com/go-logr/logr v1.4.2
github.com/golang/protobuf v1.5.4
github.com/google/go-cmp v0.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
Expand Down
126 changes: 126 additions & 0 deletions pkg/multiproject/gce/gce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package gce

import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"

"github.com/go-ini/ini"
cloudgce "k8s.io/cloud-provider-gcp/providers/gce"
"k8s.io/ingress-gce/cmd/glbc/app"
v1 "k8s.io/ingress-gce/pkg/apis/clusterslice/v1"
"k8s.io/klog/v2"
)

func init() {
// Disable pretty printing for INI files, to match default format of gce.conf.
ini.PrettyFormat = false
ini.PrettyEqual = true
ini.PrettySection = true
}

// NewGCEForClusterSlice returns a new GCE client for the given project.
// If clusterSlice is nil, it returns the default cloud associated with the cluster's project.
// It modifies the default configuration when a clusterSlice is provided.
func NewGCEForClusterSlice(defaultConfigContent string, clusterSlice *v1.ClusterSlice, logger klog.Logger) (*cloudgce.Cloud, error) {
modifiedConfigContent, err := generateConfigForClusterSlice(defaultConfigContent, clusterSlice)
if err != nil {
return nil, fmt.Errorf("failed to modify config content: %v", err)
}

// Return a new GCE client using the modified configuration content
return app.GCEClientForConfigReader(
func() io.Reader { return strings.NewReader(modifiedConfigContent) },
logger,
), nil
}

func generateConfigForClusterSlice(defaultConfigContent string, clusterSlice *v1.ClusterSlice) (string, error) {
if clusterSlice == nil {
return defaultConfigContent, nil
}

// Load the config content into an INI file
cfg, err := ini.Load([]byte(defaultConfigContent))
if err != nil {
return "", fmt.Errorf("failed to parse default config content: %w", err)
}

globalSection := cfg.Section("global")
if globalSection == nil {
return "", fmt.Errorf("global section not found in config")
}

// Update ProjectID
projectIDKey := "project-id"
globalSection.Key(projectIDKey).SetValue(clusterSlice.Spec.ProjectID)

// Update TokenURL
tokenURLKey := "token-url"
tokenURL := globalSection.Key(tokenURLKey).String()
projectNumberInt := clusterSlice.Spec.ProjectNumber
projectNumberStr := fmt.Sprintf("%d", projectNumberInt)
newTokenURL := replaceProjectNumberInTokenURL(tokenURL, projectNumberStr)
globalSection.Key(tokenURLKey).SetValue(newTokenURL)

// Update TokenBody
tokenBodyKey := "token-body"
tokenBody := globalSection.Key(tokenBodyKey).String()
newTokenBody, err := updateTokenProjectNumber(tokenBody, int(projectNumberInt))
if err != nil {
return "", fmt.Errorf("failed to update TokenBody: %v", err)
}
globalSection.Key(tokenBodyKey).SetValue(newTokenBody)

// Update NetworkName and SubnetworkName
if clusterSlice.Spec.NetworkConfig != nil {
networkNameKey := "network-name"
globalSection.Key(networkNameKey).SetValue(clusterSlice.Spec.NetworkConfig.Network)

subnetworkNameKey := "subnetwork-name"
globalSection.Key(subnetworkNameKey).SetValue(clusterSlice.Spec.NetworkConfig.DefaultSubnetwork)
}

// Write the modified config content to a string with custom options
var modifiedConfigContent bytes.Buffer
_, err = cfg.WriteTo(&modifiedConfigContent)
if err != nil {
return "", fmt.Errorf("failed to write modified config content: %v", err)
}

return modifiedConfigContent.String(), nil
}

// replaceProjectNumberInTokenURL replaces the project number in the token URL.
func replaceProjectNumberInTokenURL(tokenURL string, projectNumber string) string {
parts := strings.Split(tokenURL, "/")
for i, part := range parts {
if part == "projects" && i+1 < len(parts) {
parts[i+1] = projectNumber
break
}
}
return strings.Join(parts, "/")
}

func updateTokenProjectNumber(tokenBody string, projectNumber int) (string, error) {
var bodyMap map[string]interface{}

// Unmarshal the JSON string into a map
if err := json.Unmarshal([]byte(tokenBody), &bodyMap); err != nil {
return "", fmt.Errorf("error unmarshaling TokenBody: %v", err)
}

// Update the "projectNumber" field with the new value
bodyMap["projectNumber"] = projectNumber

// Marshal the map back into a JSON string
newTokenBodyBytes, err := json.Marshal(bodyMap)
if err != nil {
return "", fmt.Errorf("error marshaling TokenBody: %v", err)
}

return string(newTokenBodyBytes), nil
}
Loading