mirror of https://github.com/kubernetes/kops.git
Merge pull request #14018 from hakman/hetzner_server_groups
Add server group management for Hetzner
This commit is contained in:
commit
7277fc0692
|
|
@ -17,24 +17,22 @@ limitations under the License.
|
||||||
package hetznermodel
|
package hetznermodel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"k8s.io/kops/pkg/model"
|
"k8s.io/kops/pkg/model"
|
||||||
"k8s.io/kops/upup/pkg/fi"
|
"k8s.io/kops/upup/pkg/fi"
|
||||||
"k8s.io/kops/upup/pkg/fi/cloudup/hetzner"
|
"k8s.io/kops/upup/pkg/fi/cloudup/hetzner"
|
||||||
"k8s.io/kops/upup/pkg/fi/cloudup/hetznertasks"
|
"k8s.io/kops/upup/pkg/fi/cloudup/hetznertasks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerModelBuilder configures network objects
|
// ServerGroupModelBuilder configures server objects
|
||||||
type ServerModelBuilder struct {
|
type ServerGroupModelBuilder struct {
|
||||||
*HetznerModelContext
|
*HetznerModelContext
|
||||||
Lifecycle fi.Lifecycle
|
Lifecycle fi.Lifecycle
|
||||||
BootstrapScriptBuilder *model.BootstrapScriptBuilder
|
BootstrapScriptBuilder *model.BootstrapScriptBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fi.ModelBuilder = &ServerModelBuilder{}
|
var _ fi.ModelBuilder = &ServerGroupModelBuilder{}
|
||||||
|
|
||||||
func (b *ServerModelBuilder) Build(c *fi.ModelBuilderContext) error {
|
func (b *ServerGroupModelBuilder) Build(c *fi.ModelBuilderContext) error {
|
||||||
for _, ig := range b.InstanceGroups {
|
for _, ig := range b.InstanceGroups {
|
||||||
igSize := fi.Int32Value(ig.Spec.MinSize)
|
igSize := fi.Int32Value(ig.Spec.MinSize)
|
||||||
|
|
||||||
|
|
@ -48,16 +46,13 @@ func (b *ServerModelBuilder) Build(c *fi.ModelBuilderContext) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 1; i <= int(igSize); i++ {
|
serverGroup := hetznertasks.ServerGroup{
|
||||||
// hcloud-cloud-controller-manager requires hostname to be same as server name.
|
Name: fi.String(ig.Name),
|
||||||
// This means server names should not contain the cluster name (which contains "." chars"
|
|
||||||
// https://github.com/hetznercloud/hcloud-cloud-controller-manager/blob/f7d624e83c2c3475c5606306214814250922cb8a/hcloud/util.go#L39
|
|
||||||
name := ig.Name + "-" + strconv.Itoa(i)
|
|
||||||
server := hetznertasks.Server{
|
|
||||||
Name: fi.String(name),
|
|
||||||
Lifecycle: b.Lifecycle,
|
Lifecycle: b.Lifecycle,
|
||||||
SSHKey: b.LinkToSSHKey(),
|
SSHKey: b.LinkToSSHKey(),
|
||||||
Network: b.LinkToNetwork(),
|
Network: b.LinkToNetwork(),
|
||||||
|
Count: int(igSize),
|
||||||
|
Outdated: 0,
|
||||||
Location: ig.Spec.Subnets[0],
|
Location: ig.Spec.Subnets[0],
|
||||||
Size: ig.Spec.MachineType,
|
Size: ig.Spec.MachineType,
|
||||||
Image: ig.Spec.Image,
|
Image: ig.Spec.Image,
|
||||||
|
|
@ -67,8 +62,7 @@ func (b *ServerModelBuilder) Build(c *fi.ModelBuilderContext) error {
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.AddTask(&server)
|
c.AddTask(&serverGroup)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -601,7 +601,7 @@ func (c *ApplyClusterCmd) Run(ctx context.Context) error {
|
||||||
&hetznermodel.NetworkModelBuilder{HetznerModelContext: hetznerModelContext, Lifecycle: networkLifecycle},
|
&hetznermodel.NetworkModelBuilder{HetznerModelContext: hetznerModelContext, Lifecycle: networkLifecycle},
|
||||||
&hetznermodel.ExternalAccessModelBuilder{HetznerModelContext: hetznerModelContext, Lifecycle: networkLifecycle},
|
&hetznermodel.ExternalAccessModelBuilder{HetznerModelContext: hetznerModelContext, Lifecycle: networkLifecycle},
|
||||||
&hetznermodel.LoadBalancerModelBuilder{HetznerModelContext: hetznerModelContext, Lifecycle: networkLifecycle},
|
&hetznermodel.LoadBalancerModelBuilder{HetznerModelContext: hetznerModelContext, Lifecycle: networkLifecycle},
|
||||||
&hetznermodel.ServerModelBuilder{HetznerModelContext: hetznerModelContext, BootstrapScriptBuilder: bootstrapScriptBuilder, Lifecycle: clusterLifecycle},
|
&hetznermodel.ServerGroupModelBuilder{HetznerModelContext: hetznerModelContext, BootstrapScriptBuilder: bootstrapScriptBuilder, Lifecycle: clusterLifecycle},
|
||||||
)
|
)
|
||||||
case kops.CloudProviderGCE:
|
case kops.CloudProviderGCE:
|
||||||
gceModelContext := &gcemodel.GCEModelContext{
|
gceModelContext := &gcemodel.GCEModelContext{
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const (
|
||||||
TagKubernetesFirewallRole = "kops.k8s.io/firewall-role"
|
TagKubernetesFirewallRole = "kops.k8s.io/firewall-role"
|
||||||
TagKubernetesInstanceGroup = "kops.k8s.io/instance-group"
|
TagKubernetesInstanceGroup = "kops.k8s.io/instance-group"
|
||||||
TagKubernetesInstanceRole = "kops.k8s.io/instance-role"
|
TagKubernetesInstanceRole = "kops.k8s.io/instance-role"
|
||||||
|
TagKubernetesInstanceUserData = "kops.k8s.io/instance-userdata"
|
||||||
TagKubernetesVolumeRole = "kops.k8s.io/volume-role"
|
TagKubernetesVolumeRole = "kops.k8s.io/volume-role"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ package hetznertasks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/hetznercloud/hcloud-go/hcloud"
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
"k8s.io/kops/upup/pkg/fi"
|
"k8s.io/kops/upup/pkg/fi"
|
||||||
|
|
@ -27,13 +30,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// +kops:fitask
|
// +kops:fitask
|
||||||
type Server struct {
|
type ServerGroup struct {
|
||||||
Name *string
|
Name *string
|
||||||
Lifecycle fi.Lifecycle
|
Lifecycle fi.Lifecycle
|
||||||
SSHKey *SSHKey
|
SSHKey *SSHKey
|
||||||
Network *Network
|
Network *Network
|
||||||
|
|
||||||
ID *int
|
Count int
|
||||||
|
Outdated int
|
||||||
|
|
||||||
Location string
|
Location string
|
||||||
Size string
|
Size string
|
||||||
Image string
|
Image string
|
||||||
|
|
@ -46,87 +51,77 @@ type Server struct {
|
||||||
Labels map[string]string
|
Labels map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fi.CompareWithID = &Server{}
|
func (v *ServerGroup) Find(c *fi.Context) (*ServerGroup, error) {
|
||||||
|
|
||||||
func (v *Server) CompareWithID() *string {
|
|
||||||
return fi.String(strconv.Itoa(fi.IntValue(v.ID)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Server) Find(c *fi.Context) (*Server, error) {
|
|
||||||
cloud := c.Cloud.(hetzner.HetznerCloud)
|
cloud := c.Cloud.(hetzner.HetznerCloud)
|
||||||
client := cloud.ServerClient()
|
client := cloud.ServerClient()
|
||||||
|
|
||||||
// TODO(hakman): Find using label selector
|
labelSelector := []string{
|
||||||
servers, err := client.All(context.TODO())
|
fmt.Sprintf("%s=%s", hetzner.TagKubernetesClusterName, c.Cluster.Name),
|
||||||
|
fmt.Sprintf("%s=%s", hetzner.TagKubernetesInstanceGroup, v.Labels[hetzner.TagKubernetesInstanceGroup]),
|
||||||
|
}
|
||||||
|
listOptions := hcloud.ListOpts{
|
||||||
|
PerPage: 50,
|
||||||
|
LabelSelector: strings.Join(labelSelector, ","),
|
||||||
|
}
|
||||||
|
serverListOptions := hcloud.ServerListOpts{ListOpts: listOptions}
|
||||||
|
servers, err := client.AllWithOpts(context.TODO(), serverListOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(servers) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the user-data hash
|
||||||
|
userDataBytes, err := fi.ResourceAsBytes(v.UserData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userDataHash := safeBytesHash(userDataBytes)
|
||||||
|
|
||||||
|
// Add the expected user-data hash label
|
||||||
|
v.Labels[hetzner.TagKubernetesInstanceUserData] = userDataHash
|
||||||
|
|
||||||
|
actual := *v
|
||||||
|
actual.Count = 0
|
||||||
|
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
if server.Name == fi.StringValue(v.Name) {
|
if server.Labels[hetzner.TagKubernetesInstanceUserData] != userDataHash {
|
||||||
matches := &Server{
|
actual.Outdated++
|
||||||
Lifecycle: v.Lifecycle,
|
continue
|
||||||
Name: fi.String(server.Name),
|
}
|
||||||
ID: fi.Int(server.ID),
|
if server.Datacenter == nil || server.Datacenter.Location == nil || server.Datacenter.Location.Name != v.Location {
|
||||||
Labels: server.Labels,
|
actual.Outdated++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if server.ServerType == nil || server.ServerType.Name != v.Size {
|
||||||
|
actual.Outdated++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if server.Image == nil || server.Image.Name != v.Image {
|
||||||
|
actual.Outdated++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (server.PublicNet.IPv4.IP != nil) != v.EnableIPv4 {
|
||||||
|
actual.Outdated++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (server.PublicNet.IPv6.IP != nil) != v.EnableIPv6 {
|
||||||
|
actual.Outdated++
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if server.Datacenter != nil && server.Datacenter.Location != nil {
|
actual.Count++
|
||||||
matches.Location = server.Datacenter.Location.Name
|
|
||||||
}
|
|
||||||
if server.ServerType != nil {
|
|
||||||
matches.Size = server.ServerType.Name
|
|
||||||
}
|
|
||||||
if server.Image != nil {
|
|
||||||
matches.Image = server.Image.Name
|
|
||||||
}
|
|
||||||
if server.PublicNet.IPv4.IP != nil {
|
|
||||||
matches.EnableIPv4 = true
|
|
||||||
}
|
|
||||||
if server.PublicNet.IPv6.IP != nil {
|
|
||||||
matches.EnableIPv4 = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore fields that are not returned by the Hetzner Cloud API
|
return &actual, nil
|
||||||
matches.SSHKey = v.SSHKey
|
|
||||||
matches.UserData = v.UserData
|
|
||||||
|
|
||||||
// TODO: The API only returns the network ID, a new API call is required to get the network name
|
|
||||||
matches.Network = v.Network
|
|
||||||
|
|
||||||
v.ID = matches.ID
|
|
||||||
return matches, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Server) Run(c *fi.Context) error {
|
func (v *ServerGroup) Run(c *fi.Context) error {
|
||||||
return fi.DefaultDeltaRunMethod(v, c)
|
return fi.DefaultDeltaRunMethod(v, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_ *Server) CheckChanges(a, e, changes *Server) error {
|
func (_ *ServerGroup) CheckChanges(a, e, changes *ServerGroup) error {
|
||||||
if a != nil {
|
|
||||||
if changes.Name != nil {
|
|
||||||
return fi.CannotChangeField("Name")
|
|
||||||
}
|
|
||||||
if changes.ID != nil {
|
|
||||||
return fi.CannotChangeField("ID")
|
|
||||||
}
|
|
||||||
if changes.Location != "" {
|
|
||||||
return fi.CannotChangeField("Location")
|
|
||||||
}
|
|
||||||
if changes.Size != "" {
|
|
||||||
return fi.CannotChangeField("Size")
|
|
||||||
}
|
|
||||||
if changes.Image != "" {
|
|
||||||
return fi.CannotChangeField("Image")
|
|
||||||
}
|
|
||||||
if changes.UserData != nil {
|
|
||||||
return fi.CannotChangeField("UserData")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if e.Name == nil {
|
if e.Name == nil {
|
||||||
return fi.RequiredField("Name")
|
return fi.RequiredField("Name")
|
||||||
}
|
}
|
||||||
|
|
@ -142,13 +137,22 @@ func (_ *Server) CheckChanges(a, e, changes *Server) error {
|
||||||
if e.UserData == nil {
|
if e.UserData == nil {
|
||||||
return fi.RequiredField("UserData")
|
return fi.RequiredField("UserData")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_ *Server) RenderHetzner(t *hetzner.HetznerAPITarget, a, e, changes *Server) error {
|
func (_ *ServerGroup) RenderHetzner(t *hetzner.HetznerAPITarget, a, e, changes *ServerGroup) error {
|
||||||
client := t.Cloud.ServerClient()
|
client := t.Cloud.ServerClient()
|
||||||
if a == nil {
|
|
||||||
|
actualCount := 0
|
||||||
|
if a != nil {
|
||||||
|
actualCount = a.Count
|
||||||
|
}
|
||||||
|
expectedCount := e.Count
|
||||||
|
|
||||||
|
if actualCount >= expectedCount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if e.SSHKey == nil {
|
if e.SSHKey == nil {
|
||||||
return fmt.Errorf("failed to find ssh key for server %q", fi.StringValue(e.Name))
|
return fmt.Errorf("failed to find ssh key for server %q", fi.StringValue(e.Name))
|
||||||
}
|
}
|
||||||
|
|
@ -160,9 +164,18 @@ func (_ *Server) RenderHetzner(t *hetzner.HetznerAPITarget, a, e, changes *Serve
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
userDataBytes, err := fi.ResourceAsBytes(e.UserData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
userDataHash := safeBytesHash(userDataBytes)
|
||||||
|
|
||||||
|
for i := 1; i <= expectedCount-actualCount; i++ {
|
||||||
|
// Append a random/unique ID to the node name
|
||||||
|
name := fmt.Sprintf("%s-%x", e.Labels[hetzner.TagKubernetesInstanceGroup], rand.Int63())
|
||||||
|
|
||||||
opts := hcloud.ServerCreateOpts{
|
opts := hcloud.ServerCreateOpts{
|
||||||
Name: fi.StringValue(e.Name),
|
Name: name,
|
||||||
StartAfterCreate: fi.Bool(true),
|
StartAfterCreate: fi.Bool(true),
|
||||||
SSHKeys: []*hcloud.SSHKey{
|
SSHKeys: []*hcloud.SSHKey{
|
||||||
{
|
{
|
||||||
|
|
@ -191,28 +204,29 @@ func (_ *Server) RenderHetzner(t *hetzner.HetznerAPITarget, a, e, changes *Serve
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the user-data hash label
|
||||||
|
opts.Labels[hetzner.TagKubernetesInstanceUserData] = userDataHash
|
||||||
|
|
||||||
_, _, err = client.Create(context.TODO(), opts)
|
_, _, err = client.Create(context.TODO(), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
server, _, err := client.Get(context.TODO(), strconv.Itoa(fi.IntValue(a.ID)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the labels
|
|
||||||
if changes.Name != nil || len(changes.Labels) != 0 {
|
|
||||||
_, _, err := client.Update(context.TODO(), server, hcloud.ServerUpdateOpts{
|
|
||||||
Name: fi.StringValue(e.Name),
|
|
||||||
Labels: e.Labels,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func safeBytesHash(data []byte) string {
|
||||||
|
// Calculate the SHA256 checksum of the data
|
||||||
|
sum256 := sha256.Sum256(data)
|
||||||
|
|
||||||
|
// Replace the unsupported chars with supported ones
|
||||||
|
safe256 := base64.StdEncoding.EncodeToString(sum256[:])
|
||||||
|
safe256 = strings.ReplaceAll(safe256, "+", "-")
|
||||||
|
safe256 = strings.ReplaceAll(safe256, "/", "_")
|
||||||
|
|
||||||
|
// Trim the unsupported "=" padding chars
|
||||||
|
safe256 = strings.TrimRight(safe256, "=")
|
||||||
|
|
||||||
|
return fmt.Sprintf("sha256.%s", safe256)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,28 +25,28 @@ import (
|
||||||
"k8s.io/kops/upup/pkg/fi"
|
"k8s.io/kops/upup/pkg/fi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server
|
// ServerGroup
|
||||||
|
|
||||||
var _ fi.HasLifecycle = &Server{}
|
var _ fi.HasLifecycle = &ServerGroup{}
|
||||||
|
|
||||||
// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle
|
// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle
|
||||||
func (o *Server) GetLifecycle() fi.Lifecycle {
|
func (o *ServerGroup) GetLifecycle() fi.Lifecycle {
|
||||||
return o.Lifecycle
|
return o.Lifecycle
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle
|
// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle
|
||||||
func (o *Server) SetLifecycle(lifecycle fi.Lifecycle) {
|
func (o *ServerGroup) SetLifecycle(lifecycle fi.Lifecycle) {
|
||||||
o.Lifecycle = lifecycle
|
o.Lifecycle = lifecycle
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fi.HasName = &Server{}
|
var _ fi.HasName = &ServerGroup{}
|
||||||
|
|
||||||
// GetName returns the Name of the object, implementing fi.HasName
|
// GetName returns the Name of the object, implementing fi.HasName
|
||||||
func (o *Server) GetName() *string {
|
func (o *ServerGroup) GetName() *string {
|
||||||
return o.Name
|
return o.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// String is the stringer function for the task, producing readable output using fi.TaskAsString
|
// String is the stringer function for the task, producing readable output using fi.TaskAsString
|
||||||
func (o *Server) String() string {
|
func (o *ServerGroup) String() string {
|
||||||
return fi.TaskAsString(o)
|
return fi.TaskAsString(o)
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue