diff --git a/cmd/kops-controller/BUILD.bazel b/cmd/kops-controller/BUILD.bazel index 8007bf1f95..0056bbfe8a 100644 --- a/cmd/kops-controller/BUILD.bazel +++ b/cmd/kops-controller/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "//cmd/kops-controller/pkg/config:go_default_library", "//pkg/nodeidentity:go_default_library", "//pkg/nodeidentity/aws:go_default_library", + "//pkg/nodeidentity/do:go_default_library", "//pkg/nodeidentity/gce:go_default_library", "//pkg/nodeidentity/openstack:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", diff --git a/cmd/kops-controller/main.go b/cmd/kops-controller/main.go index 66b6639ae1..5d0ddd8351 100644 --- a/cmd/kops-controller/main.go +++ b/cmd/kops-controller/main.go @@ -31,6 +31,7 @@ import ( "k8s.io/kops/cmd/kops-controller/pkg/config" "k8s.io/kops/pkg/nodeidentity" nodeidentityaws "k8s.io/kops/pkg/nodeidentity/aws" + nodeidentitydo "k8s.io/kops/pkg/nodeidentity/do" nodeidentitygce "k8s.io/kops/pkg/nodeidentity/gce" nodeidentityos "k8s.io/kops/pkg/nodeidentity/openstack" ctrl "sigs.k8s.io/controller-runtime" @@ -140,6 +141,12 @@ func addNodeController(mgr manager.Manager, opt *config.Options) error { return fmt.Errorf("error building identifier: %v", err) } + case "digitalocean": + identifier, err = nodeidentitydo.New() + if err != nil { + return fmt.Errorf("error building identifier: %v", err) + } + case "": return fmt.Errorf("must specify cloud") diff --git a/pkg/model/domodel/droplets.go b/pkg/model/domodel/droplets.go index eccaa5cd9d..77c09078a5 100644 --- a/pkg/model/domodel/droplets.go +++ b/pkg/model/domodel/droplets.go @@ -73,6 +73,9 @@ func (d *DropletBuilder) Build(c *fi.ModelBuilderContext) error { clusterTagIndex := do.TagKubernetesClusterIndex + ":" + strconv.Itoa(masterIndexCount) droplet.Tags = append(droplet.Tags, clusterTagIndex) droplet.Tags = append(droplet.Tags, clusterMasterTag) + droplet.Tags = append(droplet.Tags, do.TagKubernetesClusterInstanceGroupPrefix+":"+"master-"+d.Cluster.Spec.Subnets[0].Region) + } else { + droplet.Tags = append(droplet.Tags, do.TagKubernetesClusterInstanceGroupPrefix+":"+"nodes") } userData, err := d.BootstrapScript.ResourceNodeUp(ig, d.Cluster) diff --git a/pkg/nodeidentity/do/BUILD.bazel b/pkg/nodeidentity/do/BUILD.bazel new file mode 100644 index 0000000000..107c8432eb --- /dev/null +++ b/pkg/nodeidentity/do/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["identify.go"], + importpath = "k8s.io/kops/pkg/nodeidentity/do", + visibility = ["//visibility:public"], + deps = [ + "//pkg/nodeidentity:go_default_library", + "//vendor/github.com/digitalocean/godo:go_default_library", + "//vendor/golang.org/x/oauth2:go_default_library", + "//vendor/k8s.io/api/core/v1:go_default_library", + ], +) diff --git a/pkg/nodeidentity/do/identify.go b/pkg/nodeidentity/do/identify.go new file mode 100644 index 0000000000..8780125b2e --- /dev/null +++ b/pkg/nodeidentity/do/identify.go @@ -0,0 +1,166 @@ +/* +Copyright 2019 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 do + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "strconv" + "strings" + + "golang.org/x/oauth2" + + "github.com/digitalocean/godo" + corev1 "k8s.io/api/core/v1" + "k8s.io/kops/pkg/nodeidentity" +) + +// nodeIdentifier identifies a node from EC2 +type nodeIdentifier struct { + doClient *godo.Client +} + +const ( + dropletRegionMetadataURL = "http://169.254.169.254/metadata/v1/region" + dropletTagInstanceGroupName = "kops-instancegroup" +) + +// TokenSource implements oauth2.TokenSource +type TokenSource struct { + AccessToken string +} + +// Token() returns oauth2.Token +func (t *TokenSource) Token() (*oauth2.Token, error) { + token := &oauth2.Token{ + AccessToken: t.AccessToken, + } + return token, nil +} + +// New creates and returns a nodeidentity.Identifier for Nodes running on OpenStack +func New() (nodeidentity.Identifier, error) { + region, err := getMetadataRegion() + if err != nil { + return nil, fmt.Errorf("failed to get droplet region: %s", err) + } + + godoClient, err := NewCloud(region) + if err != nil { + return nil, fmt.Errorf("failed to initialize digitalocean cloud: %s", err) + } + + return &nodeIdentifier{ + doClient: godoClient, + }, nil +} + +func getMetadataRegion() (string, error) { + return getMetadata(dropletRegionMetadataURL) +} + +// NewCloud returns a Cloud, expecting the env var DIGITALOCEAN_ACCESS_TOKEN +// NewCloud will return an err if DIGITALOCEAN_ACCESS_TOKEN is not defined +func NewCloud(region string) (*godo.Client, error) { + accessToken := os.Getenv("DIGITALOCEAN_ACCESS_TOKEN") + if accessToken == "" { + return nil, errors.New("DIGITALOCEAN_ACCESS_TOKEN is required") + } + + tokenSource := &TokenSource{ + AccessToken: accessToken, + } + + oauthClient := oauth2.NewClient(context.TODO(), tokenSource) + client := godo.NewClient(oauthClient) + + return client, nil +} + +func getMetadata(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("droplet metadata returned non-200 status code: %d", resp.StatusCode) + } + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(bodyBytes), nil +} + +// IdentifyNode queries OpenStack for the node identity information +func (i *nodeIdentifier) IdentifyNode(ctx context.Context, node *corev1.Node) (*nodeidentity.Info, error) { + providerID := node.Spec.ProviderID + if providerID == "" { + return nil, fmt.Errorf("providerID was not set for node %s", node.Name) + } + if !strings.HasPrefix(providerID, "digitalocean://") { + return nil, fmt.Errorf("providerID %q not recognized for node %s", providerID, node.Name) + } + + instanceID := strings.TrimPrefix(providerID, "digitalocean://") + if strings.HasPrefix(instanceID, "/") { + instanceID = strings.TrimPrefix(instanceID, "/") + } + + kopsGroup, err := i.getInstanceGroup(instanceID) + if err != nil { + return nil, err + } + + info := &nodeidentity.Info{} + info.InstanceGroup = kopsGroup + + return info, nil +} + +func (i *nodeIdentifier) getInstanceGroup(instanceID string) (string, error) { + + dropletID, err := strconv.Atoi(instanceID) + ctx := context.TODO() + droplet, _, err := i.doClient.Droplets.Get(ctx, dropletID) + + if err != nil { + return "", fmt.Errorf("failed to retrieve droplet via api for dropletid = %d. Error = %v", dropletID, err) + } + + for _, dropletTag := range droplet.Tags { + if strings.Contains(dropletTag, dropletTagInstanceGroupName) { + instancegrouptag := strings.SplitN(dropletTag, ":", 2) + if len(instancegrouptag) < 2 { + return "", fmt.Errorf("failed to retrieve droplet instance group tag = %s properly", dropletTag) + } + instancegroupvalue := instancegrouptag[1] + return instancegroupvalue, nil + } + } + + return "", fmt.Errorf("Could not find tag 'kops-instancegroup' from instance metadata") +} diff --git a/upup/pkg/fi/cloudup/do/cloud.go b/upup/pkg/fi/cloudup/do/cloud.go index 1fa57a677e..a0fedb210a 100644 --- a/upup/pkg/fi/cloudup/do/cloud.go +++ b/upup/pkg/fi/cloudup/do/cloud.go @@ -28,6 +28,7 @@ const TagNameEtcdClusterPrefix = "etcdCluster-" const TagNameRolePrefix = "k8s.io/role/" const TagKubernetesClusterNamePrefix = "KubernetesCluster" const TagKubernetesClusterMasterPrefix = "KubernetesCluster-Master" +const TagKubernetesClusterInstanceGroupPrefix = "kops-instancegroup" func SafeClusterName(clusterName string) string { // DO does not support . in tags / names