diff --git a/README.md b/README.md index ac6f56f11..b01882cb7 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,14 @@ Follow the instructions as detailed [here](/examples/zone-printer/README.md). To create an HTTPS ingress, follow the instructions [here](/examples/zone-printer/https.md). +## Authorization + +By default, kubemci relies on the discovery of [Application Default Credentials](https://cloud.google.com/docs/authentication/production#finding_credentials_automatically) to authorize access to GCP resources. + +As an alternative, kubemci accepts an `--access-token` argument that takes an oauth access token, +such as one generated for a service account via `gcloud --impersonate-service-account ... auth print-access-token`. +This method obviates the need to distribute private keys. + ## More information We have a [video](https://www.youtube.com/watch?v=0_Yt_1yICfk) explaining what diff --git a/app/kubemci/cmd/create.go b/app/kubemci/cmd/create.go index d42a8407d..67ab18d14 100644 --- a/app/kubemci/cmd/create.go +++ b/app/kubemci/cmd/create.go @@ -22,6 +22,7 @@ import ( "k8s.io/api/extensions/v1beta1" kubeclient "k8s.io/client-go/kubernetes" "k8s.io/kubernetes/pkg/cloudprovider/providers/gce" + // gcp is needed for GKE cluster auth to work. _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" @@ -48,6 +49,8 @@ type createOptions struct { KubeconfigFilename string // Names of the contexts to use from the kubeconfig file. KubeContexts []string + // Access token with which to access gpc resources. + AccessToken string // Name of the load balancer. // Required. LBName string @@ -98,6 +101,8 @@ func addCreateFlags(cmd *cobra.Command, options *createOptions) error { cmd.Flags().StringVarP(&options.IngressFilename, "ingress", "i", options.IngressFilename, "[required] filename containing ingress spec") cmd.Flags().StringVarP(&options.KubeconfigFilename, "kubeconfig", "k", options.KubeconfigFilename, "[required] path to kubeconfig file") cmd.Flags().StringSliceVar(&options.KubeContexts, "kubecontexts", options.KubeContexts, "[optional] contexts in the kubeconfig file to install the ingress into") + cmd.Flags().StringVarP(&options.AccessToken, "access-token", "t", options.AccessToken, "[optional] access token for gcp resources (defaults to GOOGLE_APPLICATION_CREDENTIALS).") + // TODO(nikhiljindal): Add a short flag "-p" if it seems useful. cmd.Flags().StringVarP(&options.GCPProject, "gcp-project", "", options.GCPProject, "[optional] name of the gcp project. Is fetched using gcloud config get-value project if unset here") cmd.Flags().BoolVarP(&options.ForceUpdate, "force", "f", options.ForceUpdate, "[optional] overwrite existing settings if they are different") @@ -140,7 +145,7 @@ func runCreate(options *createOptions, args []string) error { return fmt.Errorf("error in verifying static IP for ingress %s, err: %s", options.IngressFilename, err) } - cloudInterface, err := cloudinterface.NewGCECloudInterface(options.GCPProject) + cloudInterface, err := cloudinterface.NewGCECloudInterface(options.GCPProject, options.AccessToken) if err != nil { return fmt.Errorf("error in creating cloud interface: %s", err) } diff --git a/app/kubemci/cmd/delete.go b/app/kubemci/cmd/delete.go index f0a00d24b..9ab5817f8 100644 --- a/app/kubemci/cmd/delete.go +++ b/app/kubemci/cmd/delete.go @@ -21,6 +21,7 @@ import ( multierror "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "k8s.io/api/extensions/v1beta1" + // gcp is needed for GKE cluster auth to work. _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" @@ -46,6 +47,8 @@ type deleteOptions struct { KubeconfigFilename string // Names of the contexts to use from the kubeconfig file. KubeContexts []string + // Access token with which to access gpc resources. + AccessToken string // Name of the load balancer. // Required. LBName string @@ -86,6 +89,7 @@ func addDeleteFlags(cmd *cobra.Command, options *deleteOptions) error { cmd.Flags().StringVarP(&options.IngressFilename, "ingress", "i", options.IngressFilename, "[required] filename containing ingress spec") cmd.Flags().StringVarP(&options.KubeconfigFilename, "kubeconfig", "k", options.KubeconfigFilename, "[required] path to kubeconfig file") cmd.Flags().StringSliceVar(&options.KubeContexts, "kubecontexts", options.KubeContexts, "[optional] contexts in the kubeconfig file to delete the ingress from") + cmd.Flags().StringVarP(&options.AccessToken, "access-token", "t", options.AccessToken, "[optional] access token for gcp resources (defaults to GOOGLE_APPLICATION_CREDENTIALS).") // TODO(nikhiljindal): Add a short flag "-p" if it seems useful. cmd.Flags().StringVarP(&options.GCPProject, "gcp-project", "", options.GCPProject, "[optional] name of the gcp project. Is fetched using gcloud config get-value project if unset here") cmd.Flags().BoolVarP(&options.ForceDelete, "force", "f", options.ForceDelete, "[optional] delete whatever can be deleted in case of errors. This should only be used in exceptional cases (for example: when the clusters are deleted before the ingress was deleted)") @@ -124,7 +128,7 @@ func runDelete(options *deleteOptions, args []string) error { if ingErr := ingress.UnmarshallAndApplyDefaults(options.IngressFilename, options.Namespace, &ing); ingErr != nil { return fmt.Errorf("error in unmarshalling the yaml file %s, err: %s", options.IngressFilename, ingErr) } - cloudInterface, ciErr := cloudinterface.NewGCECloudInterface(options.GCPProject) + cloudInterface, ciErr := cloudinterface.NewGCECloudInterface(options.GCPProject, options.AccessToken) if ciErr != nil { return fmt.Errorf("error in creating cloud interface: %s", ciErr) } diff --git a/app/kubemci/cmd/getstatus.go b/app/kubemci/cmd/getstatus.go index 1baa3544f..7fbf80ab0 100644 --- a/app/kubemci/cmd/getstatus.go +++ b/app/kubemci/cmd/getstatus.go @@ -42,6 +42,8 @@ type getStatusOptions struct { // Required // TODO(nikhiljindal): This should be optional. Figure it out from gcloud settings. GCPProject string + // Access token with which to access gpc resources. + AccessToken string } func newCmdGetStatus(out, err io.Writer) *cobra.Command { @@ -69,6 +71,7 @@ func newCmdGetStatus(out, err io.Writer) *cobra.Command { func addGetStatusFlags(cmd *cobra.Command, options *getStatusOptions) error { // TODO(nikhiljindal): Add a short flag "-p" if it seems useful. cmd.Flags().StringVarP(&options.GCPProject, "gcp-project", "", options.GCPProject, "[optional] name of the gcp project. Is fetched using gcloud config get-value project if unset here") + cmd.Flags().StringVarP(&options.AccessToken, "access-token", "t", options.AccessToken, "[optional] access token for gcp resources (defaults to GOOGLE_APPLICATION_CREDENTIALS).") // TODO Add a verbose flag that turns on glog logging. return nil } @@ -92,7 +95,7 @@ func validateGetStatusArgs(options *getStatusOptions, args []string) error { func runGetStatus(options *getStatusOptions, args []string) error { options.LBName = args[0] - cloudInterface, err := cloudinterface.NewGCECloudInterface(options.GCPProject) + cloudInterface, err := cloudinterface.NewGCECloudInterface(options.GCPProject, options.AccessToken) if err != nil { return fmt.Errorf("error in creating cloud interface: %s", err) } diff --git a/app/kubemci/cmd/list.go b/app/kubemci/cmd/list.go index 5d0c4f01d..c06d38825 100644 --- a/app/kubemci/cmd/list.go +++ b/app/kubemci/cmd/list.go @@ -38,6 +38,8 @@ type listOptions struct { // Required // TODO(nikhiljindal): This should be optional. Figure it out from gcloud settings. GCPProject string + // Access token with which to access gpc resources. + AccessToken string } func newCmdList(out, err io.Writer) *cobra.Command { @@ -63,6 +65,7 @@ func newCmdList(out, err io.Writer) *cobra.Command { func addListFlags(cmd *cobra.Command, options *listOptions) error { cmd.Flags().StringVarP(&options.GCPProject, "gcp-project", "", options.GCPProject, "[optional] name of the gcp project. Is fetched using 'gcloud config get-value project' if unset here") + cmd.Flags().StringVarP(&options.AccessToken, "access-token", "t", options.AccessToken, "[optional] access token for gcp resources (defaults to GOOGLE_APPLICATION_CREDENTIALS).") return nil } @@ -84,7 +87,7 @@ func validateListArgs(options *listOptions, args []string) error { // runList prints a list of all mcis that we've created. func runList(options *listOptions, args []string) error { - cloudInterface, err := cloudinterface.NewGCECloudInterface(options.GCPProject) + cloudInterface, err := cloudinterface.NewGCECloudInterface(options.GCPProject, options.AccessToken) if err != nil { fmt.Println("error creating cloud interface:", err) return fmt.Errorf("error in creating cloud interface: %s", err) diff --git a/app/kubemci/cmd/remove_clusters.go b/app/kubemci/cmd/remove_clusters.go index 1bf9bc68d..b2d81c613 100644 --- a/app/kubemci/cmd/remove_clusters.go +++ b/app/kubemci/cmd/remove_clusters.go @@ -21,6 +21,7 @@ import ( multierror "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "k8s.io/api/extensions/v1beta1" + // gcp is needed for GKE cluster auth to work. _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" @@ -51,6 +52,8 @@ type removeClustersOptions struct { KubeconfigFilename string // Names of the contexts to use from the kubeconfig file. KubeContexts []string + // Access token with which to access gpc resources. + AccessToken string // Name of the load balancer. // Required. LBName string @@ -89,6 +92,7 @@ func addRemoveClustersFlags(cmd *cobra.Command, options *removeClustersOptions) cmd.Flags().StringVarP(&options.IngressFilename, "ingress", "i", options.IngressFilename, "[required] filename containing ingress spec") cmd.Flags().StringVarP(&options.KubeconfigFilename, "kubeconfig", "k", options.KubeconfigFilename, "[required] path to kubeconfig file") cmd.Flags().StringSliceVar(&options.KubeContexts, "kubecontexts", options.KubeContexts, "[optional] contexts in the kubeconfig file to remove the ingress from") + cmd.Flags().StringVarP(&options.AccessToken, "access-token", "t", options.AccessToken, "[optional] access token for gcp resources (defaults to GOOGLE_APPLICATION_CREDENTIALS).") cmd.Flags().StringVarP(&options.GCPProject, "gcp-project", "", options.GCPProject, "[required] name of the gcp project") cmd.Flags().BoolVarP(&options.ForceUpdate, "force", "f", options.ForceUpdate, "[optional] overwrite existing settings if they are different") cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", options.Namespace, "[optional] namespace for the ingress only if left unspecified by ingress spec") @@ -125,7 +129,7 @@ func runRemoveClusters(options *removeClustersOptions, args []string) error { if err := ingress.UnmarshallAndApplyDefaults(options.IngressFilename, options.Namespace, &ing); err != nil { return fmt.Errorf("error in unmarshalling the yaml file %s, err: %s", options.IngressFilename, err) } - cloudInterface, err := cloudinterface.NewGCECloudInterface(options.GCPProject) + cloudInterface, err := cloudinterface.NewGCECloudInterface(options.GCPProject, options.AccessToken) if err != nil { err := fmt.Errorf("error in creating cloud interface: %s", err) fmt.Println(err) diff --git a/app/kubemci/pkg/gcp/cloudinterface/interface.go b/app/kubemci/pkg/gcp/cloudinterface/interface.go index 664d08d43..4182500e7 100644 --- a/app/kubemci/pkg/gcp/cloudinterface/interface.go +++ b/app/kubemci/pkg/gcp/cloudinterface/interface.go @@ -15,17 +15,18 @@ package cloudinterface import ( + "golang.org/x/oauth2" "k8s.io/kubernetes/pkg/cloudprovider/providers/gce" ) // NewGCECloudInterface returns a new GCECloud. -func NewGCECloudInterface(projectID string) (*gce.GCECloud, error) { - config := getCloudConfig(projectID) +func NewGCECloudInterface(projectID, accessToken string) (*gce.GCECloud, error) { + config := getCloudConfig(projectID, accessToken) return gce.CreateGCECloud(&config) } -func getCloudConfig(projectID string) gce.CloudConfig { - return gce.CloudConfig{ +func getCloudConfig(projectID, accessToken string) gce.CloudConfig { + c := gce.CloudConfig{ ProjectID: projectID, // TODO(nikhiljindal): Set the following properties. // ApiEndpoint @@ -33,4 +34,8 @@ func getCloudConfig(projectID string) gce.CloudConfig { // Zone, Region // NetworkName, SubnetworkName } + if len(accessToken) > 0 { + c.TokenSource = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}) + } + return c }