Merge pull request #1750 from robinpercy/cli-cloud-labels

WIP: Exposing cloud labels as a CLI option
This commit is contained in:
Justin Santa Barbara 2017-02-23 09:51:08 -05:00 committed by GitHub
commit 08419fcae8
14 changed files with 125 additions and 0 deletions

View File

@ -18,6 +18,7 @@ package main
import (
"bytes"
"encoding/csv"
"fmt"
"io"
"io/ioutil"
@ -76,6 +77,9 @@ type CreateClusterOptions struct {
// Enable/Disable Bastion Host complete setup
Bastion bool
// Specify tags for AWS instance groups
CloudLabels string
// Egress configuration - FOR TESTING ONLY
Egress string
}
@ -174,6 +178,9 @@ func NewCmdCreateCluster(f *util.Factory, out io.Writer) *cobra.Command {
// Bastion
cmd.Flags().BoolVar(&options.Bastion, "bastion", options.Bastion, "Pass the --bastion flag to enable a bastion instance group. Only applies to private topology.")
// Allow custom tags from the CLI
cmd.Flags().StringVar(&options.CloudLabels, "cloud-labels", options.CloudLabels, "A list of KV pairs used to tag all instance groups in AWS (eg \"Owner=John Doe,Team=Some Team\").")
return cmd
}
@ -297,6 +304,11 @@ func RunCreateCluster(f *util.Factory, out io.Writer, c *CreateClusterOptions) e
var masters []*api.InstanceGroup
var nodes []*api.InstanceGroup
var instanceGroups []*api.InstanceGroup
cloudLabels, err := parseCloudLabels(c.CloudLabels)
if err != nil {
return fmt.Errorf("error parsing global cloud labels: %v", err)
}
cluster.Spec.CloudLabels = cloudLabels
// Build the master subnets
// The master zones is the default set of zones unless explicitly set
@ -774,3 +786,30 @@ func trimCommonPrefix(names []string) []string {
return names
}
// parseCloudLabels takes a CSV list of key=value records and parses them into a map. Nested '='s are supported via
// quoted strings (eg `foo="bar=baz"` parses to map[string]string{"foo":"bar=baz"}. Nested commas are not supported.
func parseCloudLabels(s string) (map[string]string, error) {
// Replace commas with newlines to allow a single pass with csv.Reader.
// We can't use csv.Reader for the initial split because it would see each key=value record as a single field
// and significantly complicates using quoted fields as keys or values.
records := strings.Replace(s, ",", "\n", -1)
// Let the CSV library do the heavy-lifting in handling nested ='s
r := csv.NewReader(strings.NewReader(records))
r.Comma = '='
r.FieldsPerRecord = 2
r.LazyQuotes = false
r.TrimLeadingSpace = true
kvPairs, err := r.ReadAll()
if err != nil {
return nil, fmt.Errorf("One or more key=value pairs are malformed:\n%s\n:%v", records, err)
}
m := make(map[string]string, len(kvPairs))
for _, pair := range kvPairs {
m[pair[0]] = pair[1]
}
return m, nil
}

View File

@ -0,0 +1,49 @@
/*
Copyright 2017 The Kubernetes 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 main
import (
"testing"
)
func TestParseCloudLabels(t *testing.T) {
expect := map[string]string{"foo": "bar", "fib": "baz"}
checkParse(t, "", map[string]string{}, false)
checkParse(t, "foo=bar,fib=baz", expect, false)
checkParse(t, `foo=bar,"fib"="baz"`, expect, false)
checkParse(t, `"fo\""o"=bar,"fi\b"="baz"`,
map[string]string{`fo\"o`: "bar", `fi\b`: "baz"}, false)
checkParse(t, `fo"o=bar,fib=baz`, expect, true)
checkParse(t, `fo,o=bar,fib=baz`, expect, true)
}
func checkParse(t *testing.T, s string, expect map[string]string, shouldErr bool) {
m, err := parseCloudLabels(s)
if err != nil {
if shouldErr {
return
} else {
t.Errorf(err.Error())
}
}
for k, v := range expect {
if m[k] != v {
t.Errorf("Expected: %v, Got: %v", expect, m)
}
}
}

View File

@ -14,6 +14,7 @@ kops create cluster \
--master-security-groups sg-12345678,i-abcd1234 \
--topology private \
--networking weave \
--cloud-labels "Team=Dev,Owner=John Doe" \
--image 293135079892/k8s-1.4-debian-jessie-amd64-hvm-ebs-2016-11-16 \
${NAME}
```

View File

@ -41,6 +41,14 @@ Examples:
`--dns-zone=example.com` to use the hosted zone with a name of example.com
## cloud-labels
`cloud-labels` specifies tags for instance groups in AWS. The supported format is a CSV list of key=value pairs.
Keys and values must not contain embedded commas but they may contain equals signs ('=') as long as the field is
quoted:
* `--cloud-labels "Project=\"Name=Foo Customer=Acme\",Owner=Jane Doe"` will be parsed as {Project:"Name=Foo Customer=Acme",
Owner: "Jane Doe"}
## UpdatePolicy
Cluster.Spec.UpdatePolicy

View File

@ -19,6 +19,7 @@ kops create cluster
--bastion Pass the --bastion flag to enable a bastion instance group. Only applies to private topology.
--channel string Channel for default versions and configuration to use (default "stable")
--cloud string Cloud provider to use - gce, aws
--cloud-labels string A list of KV pairs used to tag all instance groups in AWS (eg "Owner=John Doe,Team=Some Team").
--dns string DNS hosted zone to use: public|private. Default is 'public'. (default "Public")
--dns-zone string DNS hosted zone to use (defaults to longest matching zone)
--image string Image to use

View File

@ -237,6 +237,9 @@ type ClusterSpec struct {
// API field controls how the API is exposed outside the cluster
API *AccessSpec `json:"api,omitempty"`
// Tags for AWS instance groups
CloudLabels map[string]string `json:"cloudLabels,omitempty"`
}
type AccessSpec struct {

View File

@ -235,6 +235,9 @@ type ClusterSpec struct {
// API field controls how the API is exposed outside the cluster
API *AccessSpec `json:"api,omitempty"`
// Tags for AWS instance groups
CloudLabels map[string]string `json:"cloudLabels,omitempty"`
}
type AccessSpec struct {

View File

@ -436,6 +436,7 @@ func autoConvert_v1alpha1_ClusterSpec_To_kops_ClusterSpec(in *ClusterSpec, out *
} else {
out.API = nil
}
out.CloudLabels = in.CloudLabels
return nil
}
@ -573,6 +574,7 @@ func autoConvert_kops_ClusterSpec_To_v1alpha1_ClusterSpec(in *kops.ClusterSpec,
} else {
out.API = nil
}
out.CloudLabels = in.CloudLabels
return nil
}

View File

@ -155,6 +155,9 @@ type ClusterSpec struct {
// API field controls how the API is exposed outside the cluster
API *AccessSpec `json:"api,omitempty"`
// Tags for AWS resources
CloudLabels map[string]string `json:"cloudLabels,omitempty"`
}
type AccessSpec struct {

View File

@ -472,6 +472,7 @@ func autoConvert_v1alpha2_ClusterSpec_To_kops_ClusterSpec(in *ClusterSpec, out *
} else {
out.API = nil
}
out.CloudLabels = in.CloudLabels
return nil
}
@ -623,6 +624,7 @@ func autoConvert_kops_ClusterSpec_To_v1alpha2_ClusterSpec(in *kops.ClusterSpec,
} else {
out.API = nil
}
out.CloudLabels = in.CloudLabels
return nil
}

View File

@ -134,6 +134,11 @@ func (m *KopsModelContext) NodeInstanceGroups() []*kops.InstanceGroup {
func (m *KopsModelContext) CloudTagsForInstanceGroup(ig *kops.InstanceGroup) (map[string]string, error) {
labels := make(map[string]string)
// Apply any user-specified global labels first so they can be overridden by IG-specific labels
for k, v := range m.Cluster.Spec.CloudLabels {
labels[k] = v
}
// Apply any user-specified labels
for k, v := range ig.Spec.CloudLabels {
labels[k] = v

View File

@ -10,6 +10,10 @@ spec:
loadBalancer:
type: Public
channel: stable
cloudLabels:
Owner: John Doe
dn: 'cn=John Doe: dc=example dc=com'
foo/bar: fib+baz
cloudProvider: aws
configBase: memfs://tests/private.example.com
etcdClusters:

View File

@ -8,6 +8,10 @@ spec:
loadBalancer:
type: Public
channel: stable
cloudLabels:
Owner: John Doe
dn: 'cn=John Doe: dc=example dc=com'
foo/bar: fib+baz
cloudProvider: aws
configBase: memfs://tests/private.example.com
etcdClusters:

View File

@ -12,3 +12,4 @@ MasterSecurityGroups:
- sg-exampleid3
- sg-exampleid4
KubernetesVersion: v1.4.8
cloudLabels: "Owner=John Doe,dn=\"cn=John Doe: dc=example dc=com\", foo/bar=fib+baz"