[Enhancement] clusterDelete: proper node and network handling (#437)

This comes with several fixes/improvements

- only consider containers that have the default object label (app=k3d)
- handle network deletion
  - check if there are other k3d containers connected
  - if there are only registries, disconnect them
  - if there are non-registry nodes, leave everything as it is
  - if there are any containers connected, that are not automatically
  disconnected, log a warning and continue
pull/442/head
Thorsten Klein 4 years ago committed by GitHub
parent ec20a1e549
commit 185ffcd34f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      cmd/cluster/clusterDelete.go
  2. 2
      docs/usage/guides/registries.md
  3. 54
      pkg/client/cluster.go
  4. 5
      pkg/config/transform.go
  5. 5
      pkg/runtimes/containerd/network.go
  6. 5
      pkg/runtimes/containerd/node.go
  7. 36
      pkg/runtimes/docker/network.go
  8. 43
      pkg/runtimes/docker/node.go
  9. 17
      pkg/runtimes/docker/translate.go
  10. 5
      pkg/runtimes/docker/volume.go
  11. 30
      pkg/runtimes/errors/errors.go
  12. 4
      pkg/runtimes/runtime.go
  13. 11
      pkg/util/filter.go
  14. 2
      version/version.go

@ -100,6 +100,7 @@ func parseDeleteClusterCmd(cmd *cobra.Command, args []string) []*k3d.Cluster {
if all, err := cmd.Flags().GetBool("all"); err != nil {
log.Fatalln(err)
} else if all {
log.Infoln("Deleting all clusters...")
clusters, err = client.ClusterList(cmd.Context(), runtimes.SelectedRuntime)
if err != nil {
log.Fatalln(err)

@ -67,7 +67,7 @@ Finally, we can create the cluster, mounting the CA file in the path we specifie
### Using k3d-managed registries
!!! info "Not ported yet"
!!! info "Just ported!"
The k3d-managed registry is available again as of k3d v4.0.0 (January 2021)
#### Create a dedicated registry together with your cluster

@ -39,6 +39,7 @@ import (
config "github.com/rancher/k3d/v4/pkg/config/v1alpha1"
k3drt "github.com/rancher/k3d/v4/pkg/runtimes"
"github.com/rancher/k3d/v4/pkg/runtimes/docker"
runtimeErr "github.com/rancher/k3d/v4/pkg/runtimes/errors"
"github.com/rancher/k3d/v4/pkg/types"
k3d "github.com/rancher/k3d/v4/pkg/types"
"github.com/rancher/k3d/v4/pkg/util"
@ -100,7 +101,6 @@ func ClusterRun(ctx context.Context, runtime k3drt.Runtime, clusterConfig *confi
// ClusterPrep takes care of the steps required before creating/starting the cluster containers
func ClusterPrep(ctx context.Context, runtime k3drt.Runtime, clusterConfig *config.ClusterConfig) error {
/*
* Set up contexts
* Used for (early) termination (across API boundaries)
@ -139,6 +139,7 @@ func ClusterPrep(ctx context.Context, runtime k3drt.Runtime, clusterConfig *conf
// Ensure referenced registries
for _, reg := range clusterConfig.ClusterCreateOpts.Registries.Use {
log.Debugf("Trying to find registry %s", reg.Host)
regNode, err := runtime.GetNode(ctx, &k3d.Node{Name: reg.Host})
if err != nil {
return fmt.Errorf("Failed to find registry node '%s': %+v", reg.Host, err)
@ -243,7 +244,6 @@ func ClusterPrepImageVolume(ctx context.Context, runtime k3drt.Runtime, cluster
* Cluster-Wide volumes
* - image volume (for importing images)
*/
imageVolumeName := fmt.Sprintf("%s-%s-images", k3d.DefaultObjectNamePrefix, cluster.Name)
if err := runtime.CreateVolume(ctx, imageVolumeName, map[string]string{k3d.LabelClusterName: cluster.Name}); err != nil {
log.Errorf("Failed to create image volume '%s' for cluster '%s'", imageVolumeName, cluster.Name)
@ -468,7 +468,7 @@ ClusterCreatOpts:
fmt.Sprintf("WORKER_PROCESSES=%d", len(strings.Split(ports, ","))),
},
Role: k3d.LoadBalancerRole,
Labels: k3d.DefaultObjectLabels, // TODO: createLoadBalancer: add more expressive labels
Labels: clusterCreateOpts.GlobalLabels, // TODO: createLoadBalancer: add more expressive labels
Network: cluster.Network.Name,
Restart: true,
}
@ -491,6 +491,10 @@ ClusterCreatOpts:
func ClusterDelete(ctx context.Context, runtime k3drt.Runtime, cluster *k3d.Cluster) error {
log.Infof("Deleting cluster '%s'", cluster.Name)
cluster, err := ClusterGet(ctx, runtime, cluster)
if err != nil {
return err
}
log.Debugf("Cluster Details: %+v", cluster)
failed := 0
@ -507,8 +511,32 @@ func ClusterDelete(ctx context.Context, runtime k3drt.Runtime, cluster *k3d.Clus
if !cluster.Network.External {
log.Infof("Deleting cluster network '%s'", cluster.Network.Name)
if err := runtime.DeleteNetwork(ctx, cluster.Network.Name); err != nil {
if strings.HasSuffix(err.Error(), "active endpoints") {
log.Warningf("Failed to delete cluster network '%s' because it's still in use: is there another cluster using it?", cluster.Network.Name)
if errors.Is(err, runtimeErr.ErrRuntimeNetworkNotEmpty) { // there are still containers connected to that network
connectedNodes, err := runtime.GetNodesInNetwork(ctx, cluster.Network.Name) // check, if there are any k3d nodes connected to the cluster
if err != nil {
log.Warningf("Failed to check cluster network for connected nodes: %+v", err)
}
if len(connectedNodes) > 0 { // there are still k3d-managed containers (aka nodes) connected to the network
connectedRegistryNodes := util.FilterNodesByRole(connectedNodes, k3d.RegistryRole)
if len(connectedRegistryNodes) == len(connectedNodes) { // only registry node(s) left in the network
for _, node := range connectedRegistryNodes {
log.Debugf("Disconnecting registry node %s from the network...", node.Name)
if err := runtime.DisconnectNodeFromNetwork(ctx, node, cluster.Network.Name); err != nil {
log.Warnf("Failed to disconnect registry %s from network %s", node.Name, cluster.Network.Name)
} else {
if err := runtime.DeleteNetwork(ctx, cluster.Network.Name); err != nil {
log.Warningf("Failed to delete cluster network, even after disconnecting registry node(s): %+v", err)
}
}
}
} else { // besides the registry node(s), there are still other nodes... maybe they still need a registry
log.Debugf("There are some non-registry nodes left in the network")
}
} else {
log.Warningf("Failed to delete cluster network '%s' because it's still in use: is there another cluster using it?", cluster.Network.Name)
}
} else {
log.Warningf("Failed to delete cluster network '%s': '%+v'", cluster.Network.Name, err)
}
@ -535,14 +563,29 @@ func ClusterDelete(ctx context.Context, runtime k3drt.Runtime, cluster *k3d.Clus
// ClusterList returns a list of all existing clusters
func ClusterList(ctx context.Context, runtime k3drt.Runtime) ([]*k3d.Cluster, error) {
log.Traceln("Listing Clusters...")
nodes, err := runtime.GetNodesByLabel(ctx, k3d.DefaultObjectLabels)
if err != nil {
log.Errorln("Failed to get clusters")
return nil, err
}
log.Debugf("Found %d nodes", len(nodes))
if log.GetLevel() == log.TraceLevel {
for _, node := range nodes {
log.Tracef("Found node %s of role %s", node.Name, node.Role)
}
}
nodes = NodeFilterByRoles(nodes, k3d.ClusterInternalNodeRoles, k3d.ClusterExternalNodeRoles)
log.Tracef("Found %d cluster-internal nodes", len(nodes))
if log.GetLevel() == log.TraceLevel {
for _, node := range nodes {
log.Tracef("Found cluster-internal node %s of role %s belonging to cluster %s", node.Name, node.Role, node.Labels[k3d.LabelClusterName])
}
}
clusters := []*k3d.Cluster{}
// for each node, check, if we can add it to a cluster or add the cluster if it doesn't exist yet
for _, node := range nodes {
@ -570,6 +613,7 @@ func ClusterList(ctx context.Context, runtime k3drt.Runtime) ([]*k3d.Cluster, er
log.Warnln(err)
}
}
log.Debugf("Found %d clusters", len(clusters))
return clusters, nil
}

@ -222,6 +222,11 @@ func TransformSimpleToClusterConfig(ctx context.Context, runtime runtimes.Runtim
GlobalEnv: []string{}, // empty init
}
// ensure, that we have the default object labels
for k, v := range k3d.DefaultObjectLabels {
clusterCreateOpts.GlobalLabels[k] = v
}
/*
* Registries
*/

@ -41,3 +41,8 @@ func (d Containerd) DeleteNetwork(ctx context.Context, ID string) error {
func (d Containerd) ConnectNodeToNetwork(ctx context.Context, node *k3d.Node, network string) error {
return nil
}
// DisconnectNodeFromNetwork disconnects a node from a network (u don't say :O)
func (d Containerd) DisconnectNodeFromNetwork(ctx context.Context, node *k3d.Node, network string) error {
return nil
}

@ -137,3 +137,8 @@ func (d Containerd) ExecInNode(ctx context.Context, node *k3d.Node, cmd []string
func (d Containerd) ExecInNodeGetLogs(ctx context.Context, node *k3d.Node, cmd []string) (*bufio.Reader, error) {
return nil, nil
}
// GetNodesInNetwork returns all the nodes connected to a given network
func (d Containerd) GetNodesInNetwork(ctx context.Context, network string) ([]*k3d.Node, error) {
return nil, nil
}

@ -25,12 +25,14 @@ import (
"context"
"fmt"
"net"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
runtimeErr "github.com/rancher/k3d/v4/pkg/runtimes/errors"
k3d "github.com/rancher/k3d/v4/pkg/types"
log "github.com/sirupsen/logrus"
)
@ -94,7 +96,13 @@ func (d Docker) DeleteNetwork(ctx context.Context, ID string) error {
defer docker.Close()
// (3) delete network
return docker.NetworkRemove(ctx, ID)
if err := docker.NetworkRemove(ctx, ID); err != nil {
if strings.HasSuffix(err.Error(), "active endpoints") {
return runtimeErr.ErrRuntimeNetworkNotEmpty
}
return err
}
return nil
}
// GetNetwork gets information about a network by its ID
@ -147,3 +155,29 @@ func (d Docker) ConnectNodeToNetwork(ctx context.Context, node *k3d.Node, networ
// connect container to network
return docker.NetworkConnect(ctx, networkResource.ID, container.ID, &network.EndpointSettings{})
}
// DisconnectNodeFromNetwork disconnects a node from a network (u don't say :O)
func (d Docker) DisconnectNodeFromNetwork(ctx context.Context, node *k3d.Node, networkName string) error {
// get container
container, err := getNodeContainer(ctx, node)
if err != nil {
return err
}
// get docker client
docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Errorln("Failed to create docker client")
return err
}
defer docker.Close()
// get network
networkResource, err := GetNetwork(ctx, networkName)
if err != nil {
log.Errorf("Failed to get network '%s'", networkName)
return err
}
return docker.NetworkDisconnect(ctx, networkResource.ID, container.ID, true)
}

@ -25,6 +25,7 @@ package docker
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
@ -33,6 +34,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
runtimeErr "github.com/rancher/k3d/v4/pkg/runtimes/errors"
k3d "github.com/rancher/k3d/v4/pkg/types"
log "github.com/sirupsen/logrus"
)
@ -59,6 +61,7 @@ func (d Docker) CreateNode(ctx context.Context, node *k3d.Node) error {
// DeleteNode deletes a node
func (d Docker) DeleteNode(ctx context.Context, nodeSpec *k3d.Node) error {
log.Debugf("Deleting node %s ...", nodeSpec.Name)
return removeContainer(ctx, nodeSpec.Name)
}
@ -218,7 +221,7 @@ func (d Docker) GetNode(ctx context.Context, node *k3d.Node) (*k3d.Node, error)
node, err = TranslateContainerDetailsToNode(containerDetails)
if err != nil {
log.Errorf("Failed to translate container details for node '%s' to node object", node.Name)
log.Errorf("Failed to translate container '%s' to node object", containerDetails.Name)
return node, err
}
@ -396,10 +399,44 @@ func executeInNode(ctx context.Context, node *k3d.Node, cmd []string) (*types.Hi
if execInfo.ExitCode == 0 { // success
log.Debugf("Exec process in node '%s' exited with '0'", node.Name)
return &execConnection, nil
} else { // failed
return &execConnection, fmt.Errorf("Exec process in node '%s' failed with exit code '%d'", node.Name, execInfo.ExitCode)
}
return &execConnection, fmt.Errorf("Exec process in node '%s' failed with exit code '%d'", node.Name, execInfo.ExitCode)
}
}
// GetNodesInNetwork returns all the nodes connected to a given network
func (d Docker) GetNodesInNetwork(ctx context.Context, network string) ([]*k3d.Node, error) {
// create docker client
docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Errorln("Failed to create docker client")
return nil, err
}
defer docker.Close()
net, err := GetNetwork(ctx, network)
if err != nil {
return nil, err
}
connectedNodes := []*k3d.Node{}
// loop over list of containers connected to this cluster and transform them into nodes internally
for cID := range net.Containers {
containerDetails, err := getContainerDetails(ctx, cID)
if err != nil {
return nil, err
}
node, err := TranslateContainerDetailsToNode(containerDetails)
if err != nil {
if errors.Is(err, runtimeErr.ErrRuntimeContainerUnknown) {
log.Tracef("GetNodesInNetwork: inspected non-k3d-managed container %s", containerDetails.Name)
continue
}
return nil, err
}
connectedNodes = append(connectedNodes, node)
}
return connectedNodes, nil
}

@ -31,6 +31,7 @@ import (
docker "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
runtimeErr "github.com/rancher/k3d/v4/pkg/runtimes/errors"
k3d "github.com/rancher/k3d/v4/pkg/types"
log "github.com/sirupsen/logrus"
@ -135,6 +136,22 @@ func TranslateContainerToNode(cont *types.Container) (*k3d.Node, error) {
// TranslateContainerDetailsToNode translates a docker containerJSON object into a k3d node representation
func TranslateContainerDetailsToNode(containerDetails types.ContainerJSON) (*k3d.Node, error) {
// first, make sure, that it's actually a k3d managed container by checking if it has all the default labels
for k, v := range k3d.DefaultObjectLabels {
log.Tracef("TranslateContainerDetailsToNode: Checking for default object label %s=%s", k, v)
found := false
for lk, lv := range containerDetails.Config.Labels {
if lk == k && lv == v {
found = true
break
}
}
if !found {
log.Debugf("Container %s is missing default label %s=%s in label set %+v", containerDetails.Name, k, v, containerDetails.Config.Labels)
return nil, runtimeErr.ErrRuntimeContainerUnknown
}
}
// restart -> we only set 'unless-stopped' upon cluster creation
restart := false
if containerDetails.HostConfig.RestartPolicy.IsAlways() || containerDetails.HostConfig.RestartPolicy.IsUnlessStopped() {

@ -45,11 +45,12 @@ func (d Docker) CreateVolume(ctx context.Context, name string, labels map[string
// (1) create volume
volumeCreateOptions := volume.VolumeCreateBody{
Name: name,
Labels: k3d.DefaultObjectLabels,
Labels: labels,
Driver: "local", // TODO: allow setting driver + opts
DriverOpts: map[string]string{},
}
for k, v := range labels {
for k, v := range k3d.DefaultObjectLabels {
volumeCreateOptions.Labels[k] = v
}

@ -0,0 +1,30 @@
/*
Copyright © 2020 The k3d Author(s)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package runtimes
import "errors"
// ErrRuntimeNetworkNotEmpty describes an error that occurs because a network still has containers connected to it (e.g. cannot be deleted)
var ErrRuntimeNetworkNotEmpty = errors.New("network not empty")
// ErrRuntimeContainerUnknown describes the situation, where we're inspecting a container that's not obviously managed by k3d
var ErrRuntimeContainerUnknown = errors.New("container not managed by k3d: missing default label(s)")

@ -56,6 +56,7 @@ type Runtime interface {
GetNodesByLabel(context.Context, map[string]string) ([]*k3d.Node, error)
GetNode(context.Context, *k3d.Node) (*k3d.Node, error)
GetNodeStatus(context.Context, *k3d.Node) (bool, string, error)
GetNodesInNetwork(context.Context, string) ([]*k3d.Node, error)
CreateNetworkIfNotPresent(context.Context, string) (string, bool, error) // @return NETWORK_NAME, EXISTS, ERROR
GetKubeconfig(context.Context, *k3d.Node) (io.ReadCloser, error)
DeleteNetwork(context.Context, string) error
@ -72,7 +73,8 @@ type Runtime interface {
CopyToNode(context.Context, string, string, *k3d.Node) error // @param context, source, destination, node
WriteToNode(context.Context, []byte, string, *k3d.Node) error // @param context, content, destination, node
GetHostIP(context.Context, string) (net.IP, error)
ConnectNodeToNetwork(context.Context, *k3d.Node, string) error // @param context, node, network name
ConnectNodeToNetwork(context.Context, *k3d.Node, string) error // @param context, node, network name
DisconnectNodeFromNetwork(context.Context, *k3d.Node, string) error // @param context, node, network name
}
// GetRuntime checks, if a given name is represented by an implemented k3d runtime and returns it

@ -174,3 +174,14 @@ func FilterNodes(nodes []*k3d.Node, filters []string) ([]*k3d.Node, error) {
return filteredNodes, nil
}
// FilterNodesByRole returns a stripped list of nodes which do match the given role
func FilterNodesByRole(nodes []*k3d.Node, role k3d.Role) []*k3d.Node {
filteredNodes := []*k3d.Node{}
for _, node := range nodes {
if node.Role == role {
filteredNodes = append(filteredNodes, node)
}
}
return filteredNodes
}

@ -35,7 +35,7 @@ var Version string
var HelperVersionOverride string
// K3sVersion should contain the latest version tag of k3s (hardcoded at build time)
var K3sVersion = "rancher/k3s:v1.20.0-k3s2"
var K3sVersion = "v1.20.0-k3s2"
// GetVersion returns the version for cli, it gets it from "git describe --tags" or returns "dev" when doing simple go build
func GetVersion() string {

Loading…
Cancel
Save