1
0
Fork 0
mirror of https://github.com/imjasonh/gke-auth synced 2026-07-07 00:23:18 +00:00
gke-auth/main.go
minhthong582000 8f2b6af6f1 Add --alias flag to customize kubeconfig context name
By default the context/cluster/user entries in kubeconfig are named
gke_{project}_{location}_{cluster}, which is verbose and hard to type.
Add --alias so users can set a short, memorable name instead.
2026-06-05 02:04:21 +07:00

215 lines
6 KiB
Go

package main
import (
"context"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/docker/cli/cli/config"
"golang.org/x/oauth2/google"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
var (
project = flag.String("project", "", "Name of the project")
location = flag.String("location", "", "Location of the cluster")
clusterName = flag.String("cluster", "", "Name of the cluster")
alias = flag.String("alias", "", "Alias name for the kubeconfig context (defaults to gke_{project}_{location}_{cluster})")
get = flag.Bool("get", false, "If true, print auth information")
clear = flag.Bool("clear", false, "If true, clear auth for this cluster")
configureDocker = flag.Bool("configure-docker", false, "If true, configure docker to use this token")
)
func main() {
flag.Parse()
ctx := context.Background()
if *get && *clear {
log.Fatal("cannot pass both --get and --clear")
}
if (*get || *clear) && *configureDocker {
log.Fatal("cannot pass --get or --clear with --configure-docker")
}
if *configureDocker {
if *location == "" {
log.Fatal("must pass --location when using --configure-docker")
}
cfg, err := config.Load(config.Dir())
if err != nil {
log.Fatalf("Loading docker config: %v", err)
}
if cfg.CredentialHelpers == nil {
cfg.CredentialHelpers = map[string]string{}
}
host := fmt.Sprintf("%s-docker.pkg.dev", *location)
cfg.CredentialHelpers[host] = "gke-auth"
path, err := exec.LookPath(os.Args[0])
if err != nil {
log.Fatalf("Looking up path: %v", err)
}
sl := filepath.Dir(path) + "/docker-credential-gke-auth"
if err := os.Remove(sl); err != nil && !os.IsNotExist(err) {
log.Fatalf("Removing existing symlink: %v", err)
}
if err := os.Symlink(path, filepath.Dir(path)+"/docker-credential-gke-auth"); err != nil {
log.Fatalf("Symlinking: %v", err)
}
if err := cfg.Save(); err != nil {
log.Fatalf("Saving docker config: %v", err)
}
log.Printf("Docker configured to use gke-auth for %q in %q", host, *location)
return
}
// get a token
scopes := []string{
// Base scope for GCP auth
"https://www.googleapis.com/auth/cloud-platform",
// Needed in order to use service account emails instead of unique IDs in K8S RBAC.
"https://www.googleapis.com/auth/userinfo.email",
}
ts, err := google.DefaultTokenSource(ctx, scopes...)
if err != nil {
log.Fatalf("google.DefaultTokenSource: %v", err)
}
tok, err := ts.Token()
if err != nil {
log.Fatalf("ts.Token: %v", err)
}
if strings.HasSuffix(os.Args[0], "docker-credential-gke-auth") {
urlb, err := io.ReadAll(io.LimitReader(os.Stdin, 1000))
if err != nil {
log.Fatalf("Reading stdin: %v", err)
}
if err := json.NewEncoder(os.Stdout).Encode(&struct {
ServerURL string `json:"ServerURL"`
Username string `json:"Username"`
Secret string `json:"Secret"`
}{
ServerURL: string(urlb),
Username: "oauth2accesstoken",
Secret: tok.AccessToken,
}); err != nil {
log.Fatalf("Encoding JSON: %v", err)
}
return
}
if !*get && (*project == "" || *location == "" || *clusterName == "") {
log.Fatal("must pass --project and --location and --cluster")
}
if *get {
// just print the token
if err := json.NewEncoder(os.Stdout).Encode(&clientauthv1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1",
Kind: "ExecCredential",
},
Status: &clientauthv1.ExecCredentialStatus{
ExpirationTimestamp: &metav1.Time{Time: tok.Expiry},
Token: tok.AccessToken,
},
}); err != nil {
log.Fatalf("Encoding JSON: %v", err)
}
return
}
url := fmt.Sprintf("https://container.googleapis.com/v1/projects/%s/locations/%s/clusters/%s", *project, *location, *clusterName)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Fatalf("http.NewRequest: %v", err)
}
tok.SetAuthHeader(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("http.Do: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
all, _ := io.ReadAll(resp.Body)
log.Fatalf("http.Do: %d %s %s", resp.StatusCode, resp.Status, string(all))
}
var cluster struct {
Endpoint string
MasterAuth struct {
ClusterCaCertificate string
}
}
if err := json.NewDecoder(resp.Body).Decode(&cluster); err != nil {
log.Fatalf("json.Decode: %v", err)
}
cfg, err := clientcmd.NewDefaultClientConfigLoadingRules().Load()
if err != nil {
log.Fatalf("Loading kubeconfig %v", err)
}
// get kubeconfig location
key := fmt.Sprintf("gke_%s_%s_%s", *project, *location, *clusterName)
if *alias != "" {
key = *alias
}
kcfgPath := clientcmd.RecommendedHomeFile
if auth := cfg.AuthInfos[key]; auth != nil && auth.LocationOfOrigin != "" {
kcfgPath = auth.LocationOfOrigin
}
// add user
if *clear {
cfg.AuthInfos[key] = &api.AuthInfo{}
cfg.Clusters[key] = &api.Cluster{}
cfg.Contexts[key] = &api.Context{}
cfg.CurrentContext = ""
log.Println("Auth config cleared")
} else {
cfg.AuthInfos[key] = &api.AuthInfo{
Exec: &api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1",
Command: os.Args[0],
Args: []string{"--get"},
InteractiveMode: api.NeverExecInteractiveMode,
InstallHint: "go install github.com/imjasonh/gke-auto@latest",
},
}
// add cluster
dec, err := base64.StdEncoding.DecodeString(cluster.MasterAuth.ClusterCaCertificate)
if err != nil {
log.Fatalf("Decoding CA cert: %v", err)
}
cfg.Clusters[key] = &api.Cluster{
Server: "https://" + cluster.Endpoint,
CertificateAuthorityData: dec,
}
// update current context
cfg.Contexts[key] = &api.Context{
AuthInfo: key,
Cluster: key,
}
cfg.CurrentContext = key
}
// write the file back
if err := clientcmd.WriteToFile(*cfg, kcfgPath); err != nil {
log.Fatalf("Writing kubeconfig %q: %v", kcfgPath, err)
}
}