Add reader interface
With this implementation reader is an in memory interface that allows to build clusterctl config based airshipctl documents Relates-To: #200 Closes: #200 Change-Id: If4a5fbd5c8402c958563cdfc939fc579289b0bfb
This commit is contained in:
parent
71b06db819
commit
391525a165
2
go.mod
2
go.mod
@ -23,9 +23,7 @@ require (
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/spf13/afero v1.2.2
|
||||
github.com/spf13/cobra v0.0.6
|
||||
github.com/spf13/viper v1.6.2
|
||||
github.com/stretchr/testify v1.4.0
|
||||
k8s.io/api v0.17.3
|
||||
k8s.io/apiextensions-apiserver v0.17.3
|
||||
|
@ -15,20 +15,17 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
clusterctlclient "sigs.k8s.io/cluster-api/cmd/clusterctl/client"
|
||||
clusterctlconfig "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
|
||||
clog "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
airshipv1 "opendev.org/airship/airshipctl/pkg/clusterctl/api/v1alpha1"
|
||||
"opendev.org/airship/airshipctl/pkg/clusterctl/implementations"
|
||||
"opendev.org/airship/airshipctl/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// path to file on in memory file system
|
||||
confFilePath = "/air-clusterctl.yaml"
|
||||
dummyComponentPath = "/dummy/path/v0.3.2/components.yaml"
|
||||
)
|
||||
|
||||
@ -78,38 +75,20 @@ func (c *Client) Init(kubeconfigPath string) error {
|
||||
|
||||
// newConfig returns clusterctl config client
|
||||
func newConfig(options *airshipv1.Clusterctl) (clusterctlconfig.Client, error) {
|
||||
fs := afero.NewMemMapFs()
|
||||
b := []map[string]string{}
|
||||
for _, provider := range options.Providers {
|
||||
p := map[string]string{
|
||||
"name": provider.Name,
|
||||
"type": provider.Type,
|
||||
"url": provider.URL,
|
||||
}
|
||||
// this is a workaround as cluserctl validates if URL is empty, even though it is not
|
||||
// used anywhere outside repository factory which we override
|
||||
// TODO (kkalynovskyi) we need to create issue for this in clusterctl, and remove URL
|
||||
// validation and move it to be an error during repository interface initialization
|
||||
if !provider.IsClusterctlRepository {
|
||||
p["url"] = dummyComponentPath
|
||||
provider.URL = dummyComponentPath
|
||||
}
|
||||
b = append(b, p)
|
||||
}
|
||||
cconf := map[string][]map[string]string{
|
||||
"providers": b,
|
||||
}
|
||||
data, err := yaml.Marshal(cconf)
|
||||
reader, err := implementations.NewAirshipReader(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = afero.WriteFile(fs, confFilePath, data, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set filesystem to global viper object, to make sure, that clusterctl config is read from
|
||||
// memory filesystem instead of real one.
|
||||
viper.SetFs(fs)
|
||||
return clusterctlconfig.New(confFilePath)
|
||||
return clusterctlconfig.New("", clusterctlconfig.InjectReader(reader))
|
||||
}
|
||||
|
||||
func newClusterctlClient(root string, options *airshipv1.Clusterctl) (clusterctlclient.Client, error) {
|
||||
|
@ -1,3 +1,17 @@
|
||||
/*
|
||||
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 implementations
|
||||
|
||||
import (
|
||||
@ -21,3 +35,12 @@ type ErrNoVersionsAvailable struct {
|
||||
func (e ErrNoVersionsAvailable) Error() string {
|
||||
return fmt.Sprintf(`version map is empty or not defined, %v`, e.Versions)
|
||||
}
|
||||
|
||||
// ErrValueForVariableNotSet is returned when version map is empty or not defined
|
||||
type ErrValueForVariableNotSet struct {
|
||||
Variable string
|
||||
}
|
||||
|
||||
func (e ErrValueForVariableNotSet) Error() string {
|
||||
return fmt.Sprintf("value for variable %q is not set", e.Variable)
|
||||
}
|
||||
|
97
pkg/clusterctl/implementations/reader.go
Normal file
97
pkg/clusterctl/implementations/reader.go
Normal file
@ -0,0 +1,97 @@
|
||||
/*
|
||||
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 implementations
|
||||
|
||||
import (
|
||||
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
|
||||
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
airshipv1 "opendev.org/airship/airshipctl/pkg/clusterctl/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ config.Reader = &AirshipReader{}
|
||||
|
||||
const (
|
||||
// TODO this must come as as ProviderConfigKey from clusterctl/client/config pkg
|
||||
// see https://github.com/kubernetes-sigs/cluster-api/blob/master/cmd/clusterctl/client/config/imagemeta_client.go#L27
|
||||
imagesConfigKey = "images"
|
||||
)
|
||||
|
||||
// AirshipReader provides a reader implementation backed by a map
|
||||
type AirshipReader struct {
|
||||
variables map[string]string
|
||||
}
|
||||
|
||||
// configProvider is a mirror of config.Provider, re-implemented here in order to
|
||||
// avoid circular dependencies between pkg/client/config and pkg/internal/test
|
||||
type configProvider struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Type clusterctlv1.ProviderType `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Init implementation of clusterctl reader interface
|
||||
// This is dummy method that is must be present to implement Reader interface
|
||||
func (f *AirshipReader) Init(config string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get implementation of clusterctl reader interface
|
||||
func (f *AirshipReader) Get(key string) (string, error) {
|
||||
// TODO handle empty keys
|
||||
if val, ok := f.variables[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return "", ErrValueForVariableNotSet{Variable: key}
|
||||
}
|
||||
|
||||
// Set implementation of clusterctl reader interface
|
||||
func (f *AirshipReader) Set(key, value string) {
|
||||
// TODO handle empty keys
|
||||
f.variables[key] = value
|
||||
}
|
||||
|
||||
// UnmarshalKey implementation of clusterctl reader interface
|
||||
func (f *AirshipReader) UnmarshalKey(key string, rawval interface{}) error {
|
||||
data, err := f.Get(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return yaml.Unmarshal([]byte(data), rawval)
|
||||
}
|
||||
|
||||
// NewAirshipReader returns airship implementation of clusterctl reader interface
|
||||
func NewAirshipReader(options *airshipv1.Clusterctl) (*AirshipReader, error) {
|
||||
variables := map[string]string{}
|
||||
providers := []configProvider{}
|
||||
for _, prov := range options.Providers {
|
||||
appendProvider := configProvider{
|
||||
Name: prov.Name,
|
||||
Type: clusterctlv1.ProviderType(prov.Type),
|
||||
URL: prov.URL,
|
||||
}
|
||||
providers = append(providers, appendProvider)
|
||||
}
|
||||
b, err := yaml.Marshal(providers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Add providers to config
|
||||
variables[config.ProvidersConfigKey] = string(b)
|
||||
// Add empty image configuration to config
|
||||
variables[imagesConfigKey] = ""
|
||||
return &AirshipReader{variables: variables}, nil
|
||||
}
|
271
pkg/clusterctl/implementations/reader_test.go
Normal file
271
pkg/clusterctl/implementations/reader_test.go
Normal file
@ -0,0 +1,271 @@
|
||||
/*
|
||||
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 implementations
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
|
||||
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
|
||||
|
||||
airshipv1 "opendev.org/airship/airshipctl/pkg/clusterctl/api/v1alpha1"
|
||||
)
|
||||
|
||||
func makeValidOptions() *airshipv1.Clusterctl {
|
||||
return &airshipv1.Clusterctl{
|
||||
Providers: []*airshipv1.Provider{
|
||||
{
|
||||
Name: "metal3",
|
||||
Type: "InfrastructureProvider",
|
||||
Versions: map[string]string{
|
||||
"v0.3.1": "manifests/function/capm3/v0.3.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "kubeadm",
|
||||
Type: "BootstrapProvider",
|
||||
Versions: map[string]string{
|
||||
"v0.3.3": "manifests/function/cabpk/v0.3.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "cluster-api",
|
||||
Type: "InfrastructureProvider",
|
||||
Versions: map[string]string{
|
||||
"v0.3.3": "manifests/function/capi/v0.3.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "kubeadm",
|
||||
Type: "ControlPlaneProvider",
|
||||
Versions: map[string]string{
|
||||
"v0.3.3": "manifests/function/cacpk/v0.3.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options *airshipv1.Clusterctl
|
||||
}{
|
||||
{
|
||||
// make sure we get no panic here
|
||||
name: "pass empty options",
|
||||
options: &airshipv1.Clusterctl{},
|
||||
},
|
||||
{
|
||||
name: "pass airshipctl valid config",
|
||||
options: makeValidOptions(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
options := tt.options
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader, err := NewAirshipReader(options)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, reader)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options *airshipv1.Clusterctl
|
||||
key string
|
||||
expectedErr error
|
||||
expectedResult string
|
||||
}{
|
||||
{
|
||||
// make sure we get no panic here
|
||||
name: "pass empty options",
|
||||
options: &airshipv1.Clusterctl{},
|
||||
key: "FOO",
|
||||
expectedErr: ErrValueForVariableNotSet{Variable: "FOO"},
|
||||
},
|
||||
{
|
||||
name: "pass airshipctl valid config",
|
||||
options: makeValidOptions(),
|
||||
key: "providers",
|
||||
expectedErr: nil,
|
||||
expectedResult: `- name: metal3
|
||||
type: InfrastructureProvider
|
||||
- name: kubeadm
|
||||
type: BootstrapProvider
|
||||
- name: cluster-api
|
||||
type: InfrastructureProvider
|
||||
- name: kubeadm
|
||||
type: ControlPlaneProvider
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader, err := NewAirshipReader(tt.options)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reader)
|
||||
value, err := reader.Get(tt.key)
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
assert.Equal(t, tt.expectedResult, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setKey string
|
||||
setGetValue string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
// should return empty string
|
||||
name: "set simple key",
|
||||
setKey: "FOO",
|
||||
expectedErr: nil,
|
||||
setGetValue: "",
|
||||
},
|
||||
{
|
||||
name: "set providers",
|
||||
setKey: "providers",
|
||||
expectedErr: nil,
|
||||
setGetValue: `- name: metal3
|
||||
type: InfrastructureProvider
|
||||
- name: kubeadm
|
||||
type: BootstrapProvider
|
||||
- name: cluster-api
|
||||
type: InfrastructureProvider
|
||||
- name: kubeadm
|
||||
type: ControlPlaneProvider
|
||||
`,
|
||||
},
|
||||
{
|
||||
// set empty
|
||||
name: "empty key",
|
||||
setKey: "",
|
||||
setGetValue: "some key",
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader, err := NewAirshipReader(&airshipv1.Clusterctl{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reader)
|
||||
reader.Set(tt.setKey, tt.setGetValue)
|
||||
result, err := reader.Get(tt.setKey)
|
||||
require.Equal(t, tt.expectedErr, err)
|
||||
assert.Equal(t, tt.setGetValue, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test verifies that options provider returns
|
||||
func TestUnmarshalProviders(t *testing.T) {
|
||||
options := &airshipv1.Clusterctl{
|
||||
Providers: []*airshipv1.Provider{
|
||||
{
|
||||
Name: config.Metal3ProviderName,
|
||||
Type: string(clusterctlv1.InfrastructureProviderType),
|
||||
},
|
||||
{
|
||||
Name: config.KubeadmBootstrapProviderName,
|
||||
Type: string(clusterctlv1.BootstrapProviderType),
|
||||
},
|
||||
{
|
||||
Name: config.ClusterAPIProviderName,
|
||||
Type: string(clusterctlv1.CoreProviderType),
|
||||
},
|
||||
{
|
||||
Name: config.KubeadmControlPlaneProviderName,
|
||||
Type: string(clusterctlv1.ControlPlaneProviderType),
|
||||
},
|
||||
},
|
||||
}
|
||||
providers := []configProvider{}
|
||||
reader, err := NewAirshipReader(options)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reader)
|
||||
// check if we can unmarshal provider key into correct struct
|
||||
err = reader.UnmarshalKey(config.ProvidersConfigKey, &providers)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, providers, 4)
|
||||
for _, actualProvider := range providers {
|
||||
assert.NotNil(t, options.Provider(actualProvider.Name, actualProvider.Type))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expectErr bool
|
||||
variables map[string]string
|
||||
getKey string
|
||||
unmarshal interface{}
|
||||
}{
|
||||
{
|
||||
name: "unmarshal into nil",
|
||||
getKey: "Foo",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "value doesn't exist",
|
||||
getKey: "Foo",
|
||||
variables: map[string]string{},
|
||||
unmarshal: []configProvider{},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "value doesn't exist",
|
||||
getKey: "foo",
|
||||
expectErr: false,
|
||||
variables: map[string]string{
|
||||
"foo": "foo: bar",
|
||||
},
|
||||
unmarshal: &struct {
|
||||
Foo string `json:"foo,omitempty"`
|
||||
}{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader, err := NewAirshipReader(&airshipv1.Clusterctl{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reader)
|
||||
reader.variables = tt.variables
|
||||
if tt.expectErr {
|
||||
assert.Error(t, reader.UnmarshalKey(tt.getKey, tt.unmarshal))
|
||||
} else {
|
||||
assert.NoError(t, reader.UnmarshalKey(tt.getKey, tt.unmarshal))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This test is simply for test coverage of the Reader interface
|
||||
func TestInit(t *testing.T) {
|
||||
reader, err := NewAirshipReader(&airshipv1.Clusterctl{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, reader)
|
||||
assert.NoError(t, reader.Init("anything"))
|
||||
}
|
@ -1,3 +1,17 @@
|
||||
/*
|
||||
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 implementations
|
||||
|
||||
import (
|
||||
@ -60,12 +74,14 @@ func (r *Repository) GetFile(version string, filePath string) ([]byte, error) {
|
||||
// default should be latest
|
||||
version = r.defaultVersion
|
||||
}
|
||||
|
||||
path, ok := r.versions[version]
|
||||
if !ok {
|
||||
return nil, ErrVersionNotDefined{Version: version}
|
||||
}
|
||||
|
||||
bundle, err := document.NewBundleByPath(filepath.Join(r.root, path))
|
||||
kustomizePath := filepath.Join(r.root, path)
|
||||
log.Debugf("Building cluster-api provider component documents from kustomize path at %s", kustomizePath)
|
||||
bundle, err := document.NewBundleByPath(kustomizePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -101,8 +117,9 @@ func NewRepository(root string, versions map[string]string) (repository.Reposito
|
||||
availableSemVersion, err := version.ParseSemantic(ver)
|
||||
if err != nil {
|
||||
// ignore and delete version if we can't parse it.
|
||||
log.Debugf(`Invalid version %s in repository versions map %q, ignoring it. Version must obey the syntax,
|
||||
semantics of the "Semantic Versioning" specification (http://semver.org/)`, ver, versions)
|
||||
fmtMsg := "Invalid version %s in repository versions map %q, ignoring it. " +
|
||||
"Version must obey the the Semantic Versioning specification (http://semver.org/)"
|
||||
log.Debugf(fmtMsg, ver, versions)
|
||||
// delete the version so actual version list is clean
|
||||
delete(versions, ver)
|
||||
continue
|
||||
|
@ -1,3 +1,17 @@
|
||||
/*
|
||||
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 implementations_test
|
||||
|
||||
import (
|
||||
|
Loading…
Reference in New Issue
Block a user