diff --git a/infra/README.md b/infra/README.md index c9014f11d..9e1f8efab 100644 --- a/infra/README.md +++ b/infra/README.md @@ -17,7 +17,13 @@ usage: infra [] [ ...] The prometheus/test-infra deployment tool Flags: - -h, --help Show context-sensitive help (also try --help-long and --help-man). + -h, --help Show context-sensitive help (also try --help-long and + --help-man). + -f, --file=FILE ... yaml file or folder that describes the parameters for the + object that will be deployed. + -v, --vars=VARS ... When provided it will substitute the token holders in the + yaml file. Follows the standard golang template formating + - {{ .hashStable }}. Commands: help [...] diff --git a/infra/infra.go b/infra/infra.go index 469e07665..ac65a3033 100644 --- a/infra/infra.go +++ b/infra/infra.go @@ -20,6 +20,7 @@ import ( "path/filepath" "github.com/pkg/errors" + "github.com/prometheus/test-infra/pkg/provider" "github.com/prometheus/test-infra/pkg/provider/gke" "gopkg.in/alecthomas/kingpin.v2" ) @@ -27,26 +28,28 @@ import ( func main() { log.SetFlags(log.Ltime | log.Lshortfile) + dr := provider.NewDeploymentResource() + app := kingpin.New(filepath.Base(os.Args[0]), "The prometheus/test-infra deployment tool") app.HelpFlag.Short('h') + app.Flag("file", "yaml file or folder that describes the parameters for the object that will be deployed."). + Short('f'). + ExistingFilesOrDirsVar(&dr.DeploymentFiles) + app.Flag("vars", "When provided it will substitute the token holders in the yaml file. Follows the standard golang template formating - {{ .hashStable }}."). + Short('v'). + StringMapVar(&dr.FlagDeploymentVars) - g := gke.New() + g := gke.New(dr) k8sGKE := app.Command("gke", `Google container engine provider - https://cloud.google.com/kubernetes-engine/`). - Action(g.NewGKEClient) + Action(g.SetupDeploymentResources) k8sGKE.Flag("auth", "json authentication for the project. Accepts a filepath or an env variable that inlcudes tha json data. If not set the tool will use the GOOGLE_APPLICATION_CREDENTIALS env variable (export GOOGLE_APPLICATION_CREDENTIALS=service-account.json). https://cloud.google.com/iam/docs/creating-managing-service-account-keys."). PlaceHolder("service-account.json"). Short('a'). StringVar(&g.Auth) - k8sGKE.Flag("file", "yaml file or folder that describes the parameters for the object that will be deployed."). - Required(). - Short('f'). - ExistingFilesOrDirsVar(&g.DeploymentFiles) - k8sGKE.Flag("vars", "When provided it will substitute the token holders in the yaml file. Follows the standard golang template formating - {{ .hashStable }}."). - Short('v'). - StringMapVar(&g.DeploymentVars) // Cluster operations. k8sGKECluster := k8sGKE.Command("cluster", "manage GKE clusters"). + Action(g.NewGKEClient). Action(g.GKEDeploymentsParse) k8sGKECluster.Command("create", "gke cluster create -a service-account.json -f FileOrFolder"). Action(g.ClusterCreate) @@ -55,6 +58,7 @@ func main() { // Cluster node-pool operations k8sGKENodePool := k8sGKE.Command("nodepool", "manage GKE clusters nodepools"). + Action(g.NewGKEClient). Action(g.GKEDeploymentsParse) k8sGKENodePool.Command("create", "gke nodepool create -a service-account.json -f FileOrFolder"). Action(g.NodePoolCreate) @@ -67,8 +71,9 @@ func main() { // K8s resource operations. k8sGKEResource := k8sGKE.Command("resource", `Apply and delete different k8s resources - deployments, services, config maps etc.Required variables -v PROJECT_ID, -v ZONE: -west1-b -v CLUSTER_NAME`). - Action(g.NewK8sProvider). - Action(g.K8SDeploymentsParse) + Action(g.NewGKEClient). + Action(g.K8SDeploymentsParse). + Action(g.NewK8sProvider) k8sGKEResource.Command("apply", "gke resource apply -a service-account.json -f manifestsFileOrFolder -v PROJECT_ID:test -v ZONE:europe-west1-b -v CLUSTER_NAME:test -v hashStable:COMMIT1 -v hashTesting:COMMIT2"). Action(g.ResourceApply) k8sGKEResource.Command("delete", "gke resource delete -a service-account.json -f manifestsFileOrFolder -v PROJECT_ID:test -v ZONE:europe-west1-b -v CLUSTER_NAME:test -v hashStable:COMMIT1 -v hashTesting:COMMIT2"). diff --git a/pkg/provider/gke/gke.go b/pkg/provider/gke/gke.go index ac8be84c5..887a1e9c3 100644 --- a/pkg/provider/gke/gke.go +++ b/pkg/provider/gke/gke.go @@ -16,7 +16,6 @@ package gke import ( "context" "encoding/base64" - "encoding/json" "fmt" "io/ioutil" "log" @@ -43,9 +42,9 @@ import ( ) // New is the GKE constructor. -func New() *GKE { +func New(dr *provider.DeploymentResource) *GKE { return &GKE{ - DeploymentVars: make(map[string]string), + DeploymentResource: dr, } } @@ -62,11 +61,12 @@ type GKE struct { clientGKE *gke.ClusterManagerClient // The k8s provider used when we work with the manifest files. k8sProvider *k8sProvider.K8s - // DeploymentFiles files provided from the cli. + // Final DeploymentFiles files. DeploymentFiles []string - // Variables to substitute in the DeploymentFiles. - // These are also used when the command requires some variables that are not provided by the deployment file. + // Final DeploymentVars. DeploymentVars map[string]string + // DeployResource to construct DeploymentVars and DeploymentFiles + DeploymentResource *provider.DeploymentResource // Content bytes after parsing the template variables, grouped by filename. gkeResources []Resource // K8s resource.runtime objects after parsing the template variables, grouped by filename. @@ -114,6 +114,7 @@ func (c *GKE) NewGKEClient(*kingpin.ParseContext) error { // Set the auth env variable needed to the k8s client. // The client looks for this special variable name and it is the only way to set the auth for now. // TODO: Remove when the client supports an auth config option in NewDefaultClientConfig. + // https://github.com/kubernetes/kubernetes/pull/80303 os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", saFile.Name()) opts := option.WithCredentialsJSON([]byte(c.Auth)) @@ -124,13 +125,26 @@ func (c *GKE) NewGKEClient(*kingpin.ParseContext) error { } c.clientGKE = cl c.ctx = context.Background() + + return nil +} + +// SetupDeploymentResources Sets up DeploymentVars and DeploymentFiles +func (c *GKE) SetupDeploymentResources(*kingpin.ParseContext) error { + c.DeploymentFiles = c.DeploymentResource.DeploymentFiles + c.DeploymentVars = provider.MergeDeploymentVars( + c.DeploymentResource.DefaultDeploymentVars, + c.DeploymentResource.FlagDeploymentVars, + ) return nil } // GKEDeploymentsParse parses the cluster/nodepool deployment files and saves the result as bytes grouped by the filename. // Any variables passed to the cli will be replaced in the resources files following the golang text template format. func (c *GKE) GKEDeploymentsParse(*kingpin.ParseContext) error { - c.setProjectID() + if err := c.checkDeploymentVarsAndFiles(); err != nil { + return err + } deploymentResource, err := provider.DeploymentsParse(c.DeploymentFiles, c.DeploymentVars) if err != nil { @@ -144,7 +158,9 @@ func (c *GKE) GKEDeploymentsParse(*kingpin.ParseContext) error { // K8SDeploymentsParse parses the k8s objects deployment files and saves the result as k8s objects grouped by the filename. // Any variables passed to the cli will be replaced in the resources files following the golang text template format. func (c *GKE) K8SDeploymentsParse(*kingpin.ParseContext) error { - c.setProjectID() + if err := c.checkDeploymentVarsAndFiles(); err != nil { + return err + } deploymentResource, err := provider.DeploymentsParse(c.DeploymentFiles, c.DeploymentVars) if err != nil { @@ -178,19 +194,18 @@ func (c *GKE) K8SDeploymentsParse(*kingpin.ParseContext) error { return nil } -// setProjectID either from the cli arg or read it from the auth data. -func (c *GKE) setProjectID() { - if v, ok := c.DeploymentVars["PROJECT_ID"]; !ok || v == "" { - d := make(map[string]interface{}) - if err := json.Unmarshal([]byte(c.Auth), &d); err != nil { - log.Fatalf("Couldn't parse auth file: %v", err) - } - v, ok := d["project_id"].(string) - if !ok { - log.Fatal("Couldn't get project id from the auth file") +// checkDeploymentVarsAndFiles checks whether the requied deployment vars are passed. +func (c *GKE) checkDeploymentVarsAndFiles() error { + reqDepVars := []string{"PROJECT_ID", "ZONE", "CLUSTER_NAME"} + for _, k := range reqDepVars { + if v, ok := c.DeploymentVars[k]; !ok || v == "" { + return fmt.Errorf("missing required %v variable", k) } - c.DeploymentVars["PROJECT_ID"] = v } + if len(c.DeploymentFiles) == 0 { + return fmt.Errorf("missing deployment file(s)") + } + return nil } // ClusterCreate create a new cluster or applies changes to an existing cluster. @@ -501,24 +516,11 @@ func (c *GKE) AllNodepoolsDeleted(*kingpin.ParseContext) error { // NewK8sProvider sets the k8s provider used for deploying k8s manifests. func (c *GKE) NewK8sProvider(*kingpin.ParseContext) error { - projectID, ok := c.DeploymentVars["PROJECT_ID"] - if !ok { - return fmt.Errorf("missing required PROJECT_ID variable") - } - zone, ok := c.DeploymentVars["ZONE"] - if !ok { - return fmt.Errorf("missing required ZONE variable") - } - clusterID, ok := c.DeploymentVars["CLUSTER_NAME"] - if !ok { - return fmt.Errorf("missing required CLUSTER_NAME variable") - } - // Get the authentication certificate for the cluster using the GKE client. req := &containerpb.GetClusterRequest{ - ProjectId: projectID, - Zone: zone, - ClusterId: clusterID, + ProjectId: c.DeploymentVars["PROJECT_ID"], + Zone: c.DeploymentVars["ZONE"], + ClusterId: c.DeploymentVars["CLUSTER_NAME"], } rep, err := c.clientGKE.GetCluster(c.ctx, req) if err != nil { diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index be3e10d50..a5843c89a 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -31,6 +31,25 @@ const ( globalRetryTime = 10 * time.Second ) +// DeploymentResource holds list of variables and corresponding files. +type DeploymentResource struct { + // DeploymentFiles files provided from the cli. + DeploymentFiles []string + // DeploymentVars provided from the cli. + FlagDeploymentVars map[string]string + // Default DeploymentVars. + DefaultDeploymentVars map[string]string +} + +// NewDeploymentResource returns DeploymentResource with default values. +func NewDeploymentResource() *DeploymentResource { + return &DeploymentResource{ + DeploymentFiles: []string{}, + FlagDeploymentVars: map[string]string{}, + DefaultDeploymentVars: map[string]string{}, + } +} + // Resource holds the file content after parsing the template variables. type Resource struct { FileName string @@ -106,3 +125,14 @@ func DeploymentsParse(deploymentFiles []string, deploymentVars map[string]string } return deploymentObjects, nil } + +// MergeDeploymentVars merges multiple maps based on the order. +func MergeDeploymentVars(ms ...map[string]string) map[string]string { + res := map[string]string{} + for _, m := range ms { + for k, v := range m { + res[k] = v + } + } + return res +} diff --git a/pkg/provider/provider_test.go b/pkg/provider/provider_test.go new file mode 100644 index 000000000..fc27eca30 --- /dev/null +++ b/pkg/provider/provider_test.go @@ -0,0 +1,59 @@ +// Copyright 2020 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package provider + +import ( + "reflect" + "testing" +) + +func TestMergeDeploymentVars(t *testing.T) { + dv1 := map[string]string{ + "foo": "apple", + "bar": "orange", + } + dv2 := map[string]string{ + "foo": "mango", + "baz": "banana", + "buzz": "jackfruit", + } + dv3 := map[string]string{ + "foo": "grape", + "baz": "blueberry", + } + testCases := []struct { + vars []map[string]string + merged map[string]string + }{ + { + vars: []map[string]string{dv1, dv2, dv3}, + merged: map[string]string{"bar": "orange", "baz": "blueberry", "buzz": "jackfruit", "foo": "grape"}, + }, + { + vars: []map[string]string{dv3, dv2, dv1}, + merged: map[string]string{"bar": "orange", "baz": "banana", "buzz": "jackfruit", "foo": "apple"}, + }, + { + vars: []map[string]string{dv3, dv1, dv2}, + merged: map[string]string{"bar": "orange", "baz": "banana", "buzz": "jackfruit", "foo": "mango"}, + }, + } + + for _, tc := range testCases { + r := MergeDeploymentVars(tc.vars...) + if eq := reflect.DeepEqual(tc.merged, r); !eq { + t.Errorf("\nexpect %#v\ngot %#v", tc.merged, r) + } + } +}