Add clustermap object and interface

This commit adds cluster map api object, cluster map
interface and it's implementation built on top of cluster map api
object.

Important note:
ClusterMap interface needs a method to identify namespace of the
cluster, it can't be a part of api object, because real source for
cluster namespace is cluster object from cluster-api upstream lib.
This will need further design discussion on how we will find
cluster-api kind: Cluster object in our manifests. For now, there
is dummy "default" namespace being used

Change-Id: I8175f54abbe77331f0c87c0bde50857ee5c0eb1d
This commit is contained in:
Kostiantyn Kalynovskyi 2020-09-08 22:57:06 -05:00
parent 233bbda0e0
commit 0a7661ab7c
14 changed files with 305 additions and 91 deletions

View File

@ -25,7 +25,7 @@ type ClusterMap struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// Keys in this map MUST correspond to context names in kubeconfigs provided
Map map[string]*Cluster
Map map[string]*Cluster `json:"map,omitempty"`
}
// Cluster uniquely identifies a cluster and its parent cluster

View File

@ -38,6 +38,8 @@ type Builder struct {
OutputMetadataFileName string `json:"outputMetadataFileName,omitempty"`
}
// +kubebuilder:object:root=true
// ImageConfiguration structure is inherited from apimachinery TypeMeta and ObjectMeta and is a top level
// configuration structure for building image
type ImageConfiguration struct {

View File

@ -18,6 +18,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:object:root=true
// RemoteDirectConfiguration structure is inherited from apimachinery TypeMeta and ObjectMeta structures
// and defines parameters used to bootstrap the ephemeral node during the remote direct
type RemoteDirectConfiguration struct {

View File

@ -56,6 +56,21 @@ func (in *ApplyWaitOptions) DeepCopy() *ApplyWaitOptions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Builder) DeepCopyInto(out *Builder) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Builder.
func (in *Builder) DeepCopy() *Builder {
if in == nil {
return nil
}
out := new(Builder)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Cluster) DeepCopyInto(out *Cluster) {
*out = *in
@ -164,6 +179,56 @@ func (in *Clusterctl) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Container) DeepCopyInto(out *Container) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Container.
func (in *Container) DeepCopy() *Container {
if in == nil {
return nil
}
out := new(Container)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageConfiguration) DeepCopyInto(out *ImageConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Container != nil {
in, out := &in.Container, &out.Container
*out = new(Container)
**out = **in
}
if in.Builder != nil {
in, out := &in.Builder, &out.Builder
*out = new(Builder)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageConfiguration.
func (in *ImageConfiguration) DeepCopy() *ImageConfiguration {
if in == nil {
return nil
}
out := new(ImageConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ImageConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InitOptions) DeepCopyInto(out *InitOptions) {
*out = *in
@ -401,63 +466,6 @@ func (in *Provider) DeepCopy() *Provider {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ImageConfiguration) DeepCopyInto(out *ImageConfiguration) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Container.DeepCopyInto(out.Container)
in.Builder.DeepCopyInto(out.Builder)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase.
func (in *ImageConfiguration) DeepCopy() *ImageConfiguration {
if in == nil {
return nil
}
out := new(ImageConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ImageConfiguration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Container) DeepCopyInto(out *Container) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhaseConfig.
func (in *Container) DeepCopy() *Container {
if in == nil {
return nil
}
out := new(Container)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Builder) DeepCopyInto(out *Builder) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhaseConfig.
func (in *Builder) DeepCopy() *Builder {
if in == nil {
return nil
}
out := new(Builder)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RemoteDirectConfiguration) DeepCopyInto(out *RemoteDirectConfiguration) {
*out = *in
@ -465,7 +473,7 @@ func (in *RemoteDirectConfiguration) DeepCopyInto(out *RemoteDirectConfiguration
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Phase.
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteDirectConfiguration.
func (in *RemoteDirectConfiguration) DeepCopy() *RemoteDirectConfiguration {
if in == nil {
return nil

View File

@ -0,0 +1,41 @@
/*
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
https://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 clustermap
import (
"fmt"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
)
// ErrParentNotFound returned when requested cluster is not defined or doesn't have a parent
type ErrParentNotFound struct {
Child string
Map *v1alpha1.ClusterMap
}
func (e ErrParentNotFound) Error() string {
return fmt.Sprintf("failed to find a parent for cluster %s in cluster map %v", e.Child, e.Map)
}
// ErrClusterNotInMap returned when requested cluster is not defined in cluster map
type ErrClusterNotInMap struct {
Child string
Map *v1alpha1.ClusterMap
}
func (e ErrClusterNotInMap) Error() string {
return fmt.Sprintf("cluster %s is not defined in in cluster map %v", e.Child, e.Map)
}

View File

@ -0,0 +1,79 @@
/*
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
https://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 clustermap
import (
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/log"
)
// ClusterMap interface that allows to list all clusters, find its parent, namespace,
// check if dynamic kubeconfig is enabled.
// TODO use typed cluster names
type ClusterMap interface {
ParentCluster(string) (string, error)
AllClusters() []string
DynamicKubeConfig(string) bool
ClusterNamespace(string) (string, error)
}
// clusterMap allows to view clusters and relationship between them
type clusterMap struct {
apiMap *v1alpha1.ClusterMap
}
var _ ClusterMap = clusterMap{}
// NewClusterMap returns ClusterMap interface
func NewClusterMap(cMap *v1alpha1.ClusterMap) ClusterMap {
return clusterMap{apiMap: cMap}
}
// ParentCluster finds a parent cluster for provided child
func (cm clusterMap) ParentCluster(child string) (string, error) {
currentCluster, exists := cm.apiMap.Map[child]
if !exists {
return "", ErrClusterNotInMap{Child: child, Map: cm.apiMap}
}
if currentCluster.Parent == "" {
return "", ErrParentNotFound{Child: child, Map: cm.apiMap}
}
return currentCluster.Parent, nil
}
// DynamicKubeConfig check if dynamic kubeconfig is enabled for the child cluster
func (cm clusterMap) DynamicKubeConfig(child string) bool {
childCluster, exist := cm.apiMap.Map[child]
if !exist {
log.Debugf("cluster %s is not defined in cluster map %v", child, cm.apiMap)
return false
}
return childCluster.DynamicKubeConfig
}
// AllClusters returns all clusters in a map
func (cm clusterMap) AllClusters() []string {
clusters := []string{}
for k := range cm.apiMap.Map {
clusters = append(clusters, k)
}
return clusters
}
// ClusterNamespace a namespace for given cluster
// TODO implement how to get namespace for cluster
func (cm clusterMap) ClusterNamespace(clusterName string) (string, error) {
return "default", nil
}

View File

@ -0,0 +1,90 @@
/*
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
https://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 clustermap_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/cluster/clustermap"
)
func TestClusterMap(t *testing.T) {
targetCluster := "target"
ephemeraCluster := "ephemeral"
workloadCluster := "workload"
workloadClusterNoParent := "workload without parent"
apiMap := &v1alpha1.ClusterMap{
Map: map[string]*v1alpha1.Cluster{
targetCluster: {
Parent: ephemeraCluster,
DynamicKubeConfig: false,
},
ephemeraCluster: {},
workloadCluster: {
Parent: targetCluster,
DynamicKubeConfig: true,
},
workloadClusterNoParent: {
DynamicKubeConfig: true,
},
},
}
cMap := clustermap.NewClusterMap(apiMap)
require.NotNil(t, cMap)
t.Run("ephemeral parent", func(t *testing.T) {
parent, err := cMap.ParentCluster(targetCluster)
assert.NoError(t, err)
assert.Equal(t, ephemeraCluster, parent)
})
t.Run("no cluster found", func(t *testing.T) {
parent, err := cMap.ParentCluster("does not exist")
assert.Error(t, err)
assert.Equal(t, "", parent)
})
t.Run("not dynamic kubeconf target", func(t *testing.T) {
dynamic := cMap.DynamicKubeConfig(targetCluster)
assert.False(t, dynamic)
})
t.Run("dynamic kubeconf workload", func(t *testing.T) {
dynamic := cMap.DynamicKubeConfig(workloadCluster)
assert.True(t, dynamic)
})
t.Run("target parent", func(t *testing.T) {
parent, err := cMap.ParentCluster(workloadCluster)
assert.NoError(t, err)
assert.Equal(t, targetCluster, parent)
})
t.Run("ephemeral no parent", func(t *testing.T) {
parent, err := cMap.ParentCluster(ephemeraCluster)
assert.Error(t, err)
assert.Equal(t, "", parent)
})
t.Run("all clusters", func(t *testing.T) {
clusters := cMap.AllClusters()
assert.Len(t, clusters, 4)
})
}

View File

@ -190,7 +190,7 @@ func TestExecutorRun(t *testing.T) {
})
require.NoError(t, err)
ch := make(chan events.Event)
go executor.Run(ch, ifc.RunOptions{Debug: true, DryRun: true})
go executor.Run(ch, ifc.RunOptions{DryRun: true})
var actualEvt []events.Event
for evt := range ch {
actualEvt = append(actualEvt, evt)

View File

@ -17,11 +17,10 @@ package kubeconfig
import (
"path/filepath"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/cluster/clustermap"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/errors"
"opendev.org/airship/airshipctl/pkg/log"
"opendev.org/airship/airshipctl/pkg/util"
)
@ -41,7 +40,7 @@ type Builder struct {
clusterName string
root string
clusterMap *v1alpha1.ClusterMap
clusterMap clustermap.ClusterMap
}
// WithPath allows to set path to prexisting kubeconfig
@ -57,7 +56,7 @@ func (b *Builder) WithBundle(bundlePath string) *Builder {
}
// WithClusterMap allows to set a parent cluster, that can be used to extract kubeconfig for target cluster
func (b *Builder) WithClusterMap(cMap *v1alpha1.ClusterMap) *Builder {
func (b *Builder) WithClusterMap(cMap clustermap.ClusterMap) *Builder {
b.clusterMap = cMap
return b
}
@ -101,16 +100,5 @@ func (b *Builder) fromParent() bool {
if b.clusterMap == nil {
return false
}
currentCluster, exists := b.clusterMap.Map[b.clusterName]
if !exists {
log.Debugf("cluster %s is not defined in cluster map %v", b.clusterName, b.clusterMap)
return false
}
// Check if DynamicKubeConfig is enabled, if so that means, we should get kubeconfig
// for this cluster from its parent
if currentCluster.Parent == "" || !currentCluster.DynamicKubeConfig {
log.Debugf("dynamic kubeconfig or parent cluster is not set for cluster %s", b.clusterName)
return false
}
return true
return b.clusterMap.DynamicKubeConfig(b.clusterName)
}

View File

@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/cluster/clustermap"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/k8s/kubeconfig"
"opendev.org/airship/airshipctl/pkg/util"
@ -66,7 +67,7 @@ func TestBuilder(t *testing.T) {
},
}
builder := kubeconfig.NewBuilder().
WithClusterMap(clusterMap).
WithClusterMap(clustermap.NewClusterMap(clusterMap)).
WithClusterName(childCluster)
kube := builder.Build()
// This should not be implemented yet, and we need to check that we are getting there
@ -81,7 +82,7 @@ func TestBuilder(t *testing.T) {
t.Run("No current cluster, fall to default", func(t *testing.T) {
clusterMap := &v1alpha1.ClusterMap{}
builder := kubeconfig.NewBuilder().
WithClusterMap(clusterMap).
WithClusterMap(clustermap.NewClusterMap(clusterMap)).
WithClusterName("some-cluster")
kube := builder.Build()
// We should get a default value for cluster since we don't have some-cluster set
@ -92,7 +93,7 @@ func TestBuilder(t *testing.T) {
assert.Equal(t, path, actualPath)
})
t.Run("No parent cluster is defined, fall to default", func(t *testing.T) {
t.Run("Dynamic, but no parent", func(t *testing.T) {
childCluster := "child"
clusterMap := &v1alpha1.ClusterMap{
Map: map[string]*v1alpha1.Cluster{
@ -102,15 +103,15 @@ func TestBuilder(t *testing.T) {
},
}
builder := kubeconfig.NewBuilder().
WithClusterMap(clusterMap).
WithClusterMap(clustermap.NewClusterMap(clusterMap)).
WithClusterName(childCluster)
kube := builder.Build()
// We should get a default value for cluster, as we can't find parent cluster
actualPath, cleanup, err := kube.GetFile()
defer cleanup()
require.NoError(t, err)
path := filepath.Join(util.UserHomeDir(), config.AirshipConfigDir, kubeconfig.KubeconfigDefaultFileName)
assert.Equal(t, path, actualPath)
filePath, cleanup, err := kube.GetFile()
require.Error(t, err)
require.Contains(t, err.Error(), "not implemented")
assert.Equal(t, "", filePath)
require.Nil(t, cleanup)
})
t.Run("Default source", func(t *testing.T) {
@ -119,8 +120,8 @@ func TestBuilder(t *testing.T) {
// When ClusterMap is specified, but it doesn't have cluster-name defined, and no
// other sources provided,
actualPath, cleanup, err := kube.GetFile()
defer cleanup()
require.NoError(t, err)
defer cleanup()
path := filepath.Join(util.UserHomeDir(), config.AirshipConfigDir, kubeconfig.KubeconfigDefaultFileName)
assert.Equal(t, path, actualPath)
})

View File

@ -18,7 +18,7 @@ import (
"io"
"time"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/cluster/clustermap"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/events"
@ -56,7 +56,7 @@ type ExecutorConfig struct {
PhaseName string
ClusterName string
ClusterMap *v1alpha1.ClusterMap
ClusterMap clustermap.ClusterMap
ExecutorDocument document.Document
ExecutorBundle document.Bundle
AirshipConfig *config.Config

View File

@ -21,6 +21,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/cluster/clustermap"
clusterctl "opendev.org/airship/airshipctl/pkg/clusterctl/client"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/document"
@ -106,7 +107,7 @@ func (p *Cmd) GetPhase(name string) (*airshipv1.Phase, error) {
}
// GetClusterMap returns cluster map object
func (p *Cmd) GetClusterMap() (*airshipv1.ClusterMap, error) {
func (p *Cmd) GetClusterMap() (clustermap.ClusterMap, error) {
bundle, err := p.getBundle()
if err != nil {
return nil, err
@ -124,7 +125,7 @@ func (p *Cmd) GetClusterMap() (*airshipv1.ClusterMap, error) {
if err = doc.ToAPIObject(clusterMap, airshipv1.Scheme); err != nil {
return nil, err
}
return clusterMap, nil
return clustermap.NewClusterMap(clusterMap), nil
}
// GetExecutor referenced in a phase configuration

View File

@ -0,0 +1,2 @@
phase:
path: "doesnot-exist"

View File

@ -2,7 +2,7 @@ apiVersion: airshipit.org/v1alpha1
kind: ClusterMap
metadata:
name: clusterctl-v1
plan:
map:
target:
parent: ephemeral
dynamicKubeConf: false