From b57c1a2d15c4f7508388952e103e909f4f183f5c Mon Sep 17 00:00:00 2001 From: Kostiantyn Kalynovskyi Date: Mon, 27 Apr 2020 11:38:22 -0500 Subject: [PATCH] Add clusterctl interface and factory functions Client interface currently has only one Init method. Also add a constructor for the Client which is based on resource defined in clusterctl/api package and a filesystem root to construct bundle for repository interface. Root is expected to be derived from the airshipctl settings document manifest.target-path Factory functions allow to insert custom repository interface into clusterctl client Relates-To: #170 Change-Id: Ib27e73043b4776001f405d2d4e96016735c6f46e --- go.mod | 2 + pkg/clusterctl/client/client.go | 132 +++++++++++ pkg/clusterctl/client/client_test.go | 135 +++++++++++ pkg/clusterctl/client/errors.go | 38 +++ pkg/clusterctl/client/factory.go | 89 +++++++ pkg/clusterctl/client/factory_test.go | 218 ++++++++++++++++++ .../infrastructure/v0.3.1/kustomization.yaml | 2 + .../capi/infrastructure/v0.3.1/version.yaml | 14 ++ .../infrastructure/v0.3.2/kustomization.yaml | 2 + .../capi/infrastructure/v0.3.2/version.yaml | 6 + 10 files changed, 638 insertions(+) create mode 100644 pkg/clusterctl/client/client.go create mode 100644 pkg/clusterctl/client/client_test.go create mode 100644 pkg/clusterctl/client/errors.go create mode 100644 pkg/clusterctl/client/factory.go create mode 100644 pkg/clusterctl/client/factory_test.go create mode 100644 pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.1/kustomization.yaml create mode 100644 pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.1/version.yaml create mode 100644 pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.2/kustomization.yaml create mode 100644 pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.2/version.yaml diff --git a/go.mod b/go.mod index 3dbb918d4..27aac742e 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,9 @@ require ( github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // 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 diff --git a/pkg/clusterctl/client/client.go b/pkg/clusterctl/client/client.go new file mode 100644 index 000000000..98c0a023d --- /dev/null +++ b/pkg/clusterctl/client/client.go @@ -0,0 +1,132 @@ +/* + 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 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/log" +) + +const ( + // path to file on in memory file system + confFilePath = "/air-clusterctl.yaml" + dummyComponentPath = "/dummy/path/v0.3.2/components.yaml" +) + +var _ Interface = &Client{} + +// Interface is abstraction to Clusterctl +type Interface interface { + Init(kubeconfigPath string) error +} + +// Client Implements interface to Clusterctl +type Client struct { + clusterctlClient clusterctlclient.Client + initOptions clusterctlclient.InitOptions +} + +// NewClient returns instance of clusterctl client +func NewClient(root string, debug bool, options *airshipv1.Clusterctl) (Interface, error) { + if debug { + debugVerbosity := 5 + clog.SetLogger(clog.NewLogger(clog.WithThreshold(&debugVerbosity))) + } + initOptions := options.InitOptions + var cio clusterctlclient.InitOptions + if initOptions != nil { + cio = clusterctlclient.InitOptions{ + BootstrapProviders: initOptions.BootstrapProviders, + CoreProvider: initOptions.CoreProvider, + InfrastructureProviders: initOptions.InfrastructureProviders, + ControlPlaneProviders: initOptions.ControlPlaneProviders, + } + } + cclient, err := newClusterctlClient(root, options) + if err != nil { + return nil, err + } + return &Client{clusterctlClient: cclient, initOptions: cio}, nil +} + +// Init implements interface to Clusterctl +func (c *Client) Init(kubeconfigPath string) error { + log.Print("Starting cluster-api initiation") + c.initOptions.Kubeconfig = kubeconfigPath + _, err := c.clusterctlClient.Init(c.initOptions) + return err +} + +// 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 + } + b = append(b, p) + } + cconf := map[string][]map[string]string{ + "providers": b, + } + data, err := yaml.Marshal(cconf) + 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) +} + +func newClusterctlClient(root string, options *airshipv1.Clusterctl) (clusterctlclient.Client, error) { + cconf, err := newConfig(options) + if err != nil { + return nil, err + } + rf := RepositoryFactory{ + root: root, + Options: options, + ConfigClient: cconf, + } + // option config factory + ocf := clusterctlclient.InjectConfig(cconf) + // option repository factory + orf := clusterctlclient.InjectRepositoryFactory(rf.ClientRepositoryFactory()) + // options cluster client factory + occf := clusterctlclient.InjectClusterClientFactory(rf.ClusterClientFactory()) + return clusterctlclient.New("", ocf, orf, occf) +} diff --git a/pkg/clusterctl/client/client_test.go b/pkg/clusterctl/client/client_test.go new file mode 100644 index 000000000..178e7893f --- /dev/null +++ b/pkg/clusterctl/client/client_test.go @@ -0,0 +1,135 @@ +/* + 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 client + +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/yaml" + + airshipv1 "opendev.org/airship/airshipctl/pkg/clusterctl/api/v1alpha1" +) + +var ( + testConfig = `apiVersion: airshipit.org/v1alpha1 +kind: Clusterctl +metadata: + labels: + airshipit.org/deploy-k8s: "false" + name: clusterctl-v1 +init-options: {} +providers: +- name: "aws" + type: "InfrastructureProvider" + url: "/manifests/capi/infra/aws/v0.3.0" + clusterctl-repository: true +- name: "custom-infra" + type: "InfrastructureProvider" + url: "/manifests/capi/custom-infra/aws/v0.3.0" + clusterctl-repository: true +- name: "custom-airship-infra" + type: "InfrastructureProvider" + versions: + v0.3.1: functions/capi/infrastructure/v0.3.1 + v0.3.2: functions/capi/infrastructure/v0.3.2` +) + +func TestNewConfig(t *testing.T) { + tests := []struct { + name string + conf *airshipv1.Clusterctl + presentProvider string + presentType string + expectedURL string + }{ + { + name: "clusterctl single repo", + presentProvider: "kubeadm", + presentType: "BootstrapProvider", + expectedURL: "/home/providers/kubeadm/v0.3.5/components.yaml", + conf: &airshipv1.Clusterctl{ + + Providers: []*airshipv1.Provider{ + { + Name: "kubeadm", + URL: "/home/providers/kubeadm/v0.3.5/components.yaml", + Type: "BootstrapProvider", + IsClusterctlRepository: true, + }, + }, + }, + }, + { + name: "multiple repos with airship", + presentProvider: "airship-repo", + presentType: "InfrastructureProvider", + expectedURL: dummyComponentPath, + conf: &airshipv1.Clusterctl{ + + Providers: []*airshipv1.Provider{ + { + Name: "airship-repo", + URL: "/home/providers/my-repo/v0.3.5/components.yaml", + Type: "InfrastructureProvider", + IsClusterctlRepository: false, + Versions: map[string]string{ + "v0.3.1": "some-path", + }, + }, + { + Name: "kubeadm", + URL: "/home/providers/kubeadm/v0.3.5/components.yaml", + Type: "BootstrapProvider", + IsClusterctlRepository: true, + }, + }, + }, + }, + } + for _, tt := range tests { + conf := tt.conf + url := tt.expectedURL + provName := tt.presentProvider + provType := tt.presentType + t.Run(tt.name, func(t *testing.T) { + got, err := newConfig(conf) + require.NoError(t, err) + providerClient := got.Providers() + provider, err := providerClient.Get(provName, clusterctlv1.ProviderType(provType)) + require.NoError(t, err) + assert.Equal(t, url, provider.URL()) + }) + } +} + +func TestNewClientEmptyOptions(t *testing.T) { + c := &airshipv1.Clusterctl{} + client, err := NewClient("", true, c) + require.NoError(t, err) + require.NotNil(t, client) +} + +func TestNewClient(t *testing.T) { + c := &airshipv1.Clusterctl{} + err := yaml.Unmarshal([]byte(testConfig), c) + require.NoError(t, err) + + client, err := NewClient("", true, c) + require.NoError(t, err) + require.NotNil(t, client) +} diff --git a/pkg/clusterctl/client/errors.go b/pkg/clusterctl/client/errors.go new file mode 100644 index 000000000..ccd6d7431 --- /dev/null +++ b/pkg/clusterctl/client/errors.go @@ -0,0 +1,38 @@ +/* + 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 client + +import ( + "fmt" +) + +// ErrProviderNotDefined is returned when wrong AuthType is provided +type ErrProviderNotDefined struct { + ProviderName string +} + +func (e ErrProviderNotDefined) Error() string { + return fmt.Sprintf("provider %s is not defined in Clusterctl document", e.ProviderName) +} + +// ErrProviderRepoNotFound is returned when wrong AuthType is provided +type ErrProviderRepoNotFound struct { + ProviderName string + ProviderType string +} + +func (e ErrProviderRepoNotFound) Error() string { + return fmt.Sprintf("failed to find repository for provider %s of type %s", e.ProviderName, e.ProviderType) +} diff --git a/pkg/clusterctl/client/factory.go b/pkg/clusterctl/client/factory.go new file mode 100644 index 000000000..f79837704 --- /dev/null +++ b/pkg/clusterctl/client/factory.go @@ -0,0 +1,89 @@ +/* + 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 client + +import ( + "sigs.k8s.io/cluster-api/cmd/clusterctl/client" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository" + + airshipv1 "opendev.org/airship/airshipctl/pkg/clusterctl/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/clusterctl/implementations" + "opendev.org/airship/airshipctl/pkg/log" +) + +// RepositoryFactory returns an injection factory to work with clusterctl client +type RepositoryFactory struct { + root string + Options *airshipv1.Clusterctl + ConfigClient config.Client +} + +// ClusterClientFactory returns cluster factory function for clusterctl client +func (f RepositoryFactory) ClusterClientFactory() client.ClusterClientFactory { + return func(kubeconfig string) (cluster.Client, error) { + o := cluster.InjectRepositoryFactory(f.repoFactoryClusterClient()) + return cluster.New(kubeconfig, f.ConfigClient, o), nil + } +} + +// ClientRepositoryFactory returns repo factory function for clusterctl client +func (f RepositoryFactory) ClientRepositoryFactory() client.RepositoryClientFactory { + return f.repoFactory +} + +// These two functions are basically the same, but have different with signatures +func (f RepositoryFactory) repoFactoryClusterClient() cluster.RepositoryClientFactory { + return func(provider config.Provider, + configClient config.Client, + options ...repository.Option, + ) (repository.Client, error) { + return f.repoFactory(provider) + } +} + +func (f RepositoryFactory) repoFactory(provider config.Provider) (repository.Client, error) { + name := provider.Name() + repoType := provider.Type() + airProv := f.Options.Provider(name, repoType) + if airProv == nil { + return nil, ErrProviderRepoNotFound{ProviderName: name, ProviderType: string(repoType)} + } + // if repository is not clusterctl type, construct an airshipctl implementation of repository interface + if !airProv.IsClusterctlRepository { + // Get repository version map + versions := airProv.Versions + if len(versions) == 0 { + return nil, ErrProviderRepoNotFound{ProviderName: name, ProviderType: string(repoType)} + } + // construct a repository for this provider using root and version map + repo, err := implementations.NewRepository(f.root, versions) + if err != nil { + return nil, err + } + // inject repository into repository client + o := repository.InjectRepository(repo) + log.Printf("Creating arishipctl repository implementation interface for provider %s of type %s\n", + provider.Name(), + provider.Type()) + return repository.New(provider, f.ConfigClient, o) + } + log.Printf("Creating clusterctl repository implementation interface for provider %s of type %s\n", + provider.Name(), + provider.Type()) + // if repository is clusterctl pass, simply use default clusterctl repository interface + return repository.New(provider, f.ConfigClient) +} diff --git a/pkg/clusterctl/client/factory_test.go b/pkg/clusterctl/client/factory_test.go new file mode 100644 index 000000000..0fb532655 --- /dev/null +++ b/pkg/clusterctl/client/factory_test.go @@ -0,0 +1,218 @@ +/* + 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 client + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + clusterctlconfig "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" + "sigs.k8s.io/yaml" + + airshipv1 "opendev.org/airship/airshipctl/pkg/clusterctl/api/v1alpha1" +) + +const ( + testDataDir = "testdata" +) + +var ( + testConfigFactory = `apiVersion: airshipit.org/v1alpha1 +kind: Clusterctl +metadata: + labels: + airshipit.org/deploy-k8s: "false" + name: clusterctl-v1 +init-options: {} +providers: + - name: "aws" + type: "InfrastructureProvider" + url: "/manifests/capi/infra/infrastructure-aws/v0.3.0/components.yaml" + clusterctl-repository: true + - name: "custom-infra" + type: "InfrastructureProvider" + url: "/manifests/capi/infra/infrastructure-custom-infra/v0.3.0/components.yaml" + clusterctl-repository: true + - name: "custom-airship-infra" + type: "InfrastructureProvider" + versions: + v0.3.1: functions/capi/infrastructure/v0.3.1 + v0.3.2: functions/capi/infrastructure/v0.3.2` +) + +func testOptions(t *testing.T, input string) *airshipv1.Clusterctl { + t.Helper() + o := &airshipv1.Clusterctl{} + err := yaml.Unmarshal([]byte(input), o) + require.NoError(t, err) + return o +} + +func testNewConfig(t *testing.T, o *airshipv1.Clusterctl) clusterctlconfig.Client { + t.Helper() + configClient, err := newConfig(o) + require.NoError(t, err) + require.NotNil(t, configClient) + return configClient +} + +// TestFactory checks if airship repository interface is selected for providers that are not +// of airship type, and that this interface methods return correct components +func TestFactory(t *testing.T) { + o := testOptions(t, testConfigFactory) + configClient := testNewConfig(t, o) + + factory := RepositoryFactory{ + root: testDataDir, + Options: o, + ConfigClient: configClient, + } + repoFactory := factory.ClientRepositoryFactory() + pclient := configClient.Providers() + tests := []struct { + name string + expectedVersions []string + useVersion string + useName string + useType string + expectErr bool + expectedNamespace string + }{ + { + name: "custom airship v1", + expectedVersions: []string{"v0.3.1", "v0.3.2"}, + useVersion: "v0.3.1", + useName: "custom-airship-infra", + useType: "InfrastructureProvider", + expectErr: false, + expectedNamespace: "version-one", + }, + { + name: "custom airship v2", + expectedVersions: []string{"v0.3.1", "v0.3.2"}, + useVersion: "v0.3.2", + useName: "custom-airship-infra", + useType: "InfrastructureProvider", + expectErr: false, + expectedNamespace: "version-two", + }, + } + for _, tt := range tests { + expectedVersions := tt.expectedVersions + useVersion := tt.useVersion + expectErr := tt.expectErr + useName := tt.useName + useType := tt.useType + expectedNamespace := tt.expectedNamespace + t.Run(tt.name, func(t *testing.T) { + provider, err := pclient.Get(useName, clusterctlv1.ProviderType(useType)) + require.NoError(t, err) + require.NotNil(t, provider) + repo, err := repoFactory(provider) + require.NoError(t, err) + require.NotNil(t, repo) + versions, err := repo.GetVersions() + require.NoError(t, err) + sort.Strings(expectedVersions) + sort.Strings(versions) + assert.Equal(t, dummyComponentPath, repo.URL()) + assert.Equal(t, expectedVersions, versions) + components := repo.Components() + require.NotNil(t, components) + // namespaces are left blank, since namespace is provided in the document set + component, err := components.Get(useVersion, "", "") + require.NoError(t, err) + require.NotNil(t, component) + + b, err := component.Yaml() + if expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + actualNamespace := &v1.Namespace{} + err = yaml.Unmarshal(b, actualNamespace) + require.NoError(t, err) + assert.Equal(t, expectedNamespace, actualNamespace.GetName()) + } + }) + } +} + +func TestClientRepositoryFactory(t *testing.T) { + o := testOptions(t, testConfigFactory) + configClient := testNewConfig(t, o) + + factory := RepositoryFactory{ + root: testDataDir, + Options: o, + ConfigClient: configClient, + } + clusterclientFactory := factory.ClusterClientFactory() + clusterClient, err := clusterclientFactory("testdata/kubeconfig.yaml") + assert.NoError(t, err) + assert.NotNil(t, clusterClient) +} + +func TestRepoFactoryFunction(t *testing.T) { + o := testOptions(t, testConfigFactory) + configClient := testNewConfig(t, o) + + factory := RepositoryFactory{ + root: testDataDir, + Options: o, + ConfigClient: configClient, + } + + pclient := configClient.Providers() + provider, err := pclient.Get("custom-airship-infra", "InfrastructureProvider") + require.NoError(t, err) + repoClient, err := factory.repoFactory(provider) + require.NoError(t, err) + require.NotNil(t, repoClient) + + versions, err := repoClient.GetVersions() + expectedVersions := []string{"v0.3.1", "v0.3.2"} + sort.Strings(versions) + sort.Strings(expectedVersions) + require.NoError(t, err) + assert.Equal(t, expectedVersions, versions) +} + +func TestClusterctlRepoFactoryFunction(t *testing.T) { + o := testOptions(t, testConfigFactory) + configClient := testNewConfig(t, o) + + factory := RepositoryFactory{ + root: testDataDir, + Options: o, + ConfigClient: configClient, + } + + pclient := configClient.Providers() + provider, err := pclient.Get("aws", "InfrastructureProvider") + require.NoError(t, err) + repoClient, err := factory.repoFactory(provider) + require.NoError(t, err) + require.NotNil(t, repoClient) + // try to read directory list defined by repoClient.URL() and fail + _, err = repoClient.GetVersions() + assert.Error(t, err) + // Verify clusterctl failed during reading file, note: os.IsNotExist doesn't work here + assert.Contains(t, err.Error(), "no such file or directory") +} diff --git a/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.1/kustomization.yaml b/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.1/kustomization.yaml new file mode 100644 index 000000000..3ee92e092 --- /dev/null +++ b/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.1/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - version.yaml \ No newline at end of file diff --git a/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.1/version.yaml b/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.1/version.yaml new file mode 100644 index 000000000..8ed26986a --- /dev/null +++ b/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.1/version.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: airshipit.org/v1alpha1 +kind: Testversion +metadata: + name: version-1 +spec: + version: v0.3.1 +--- +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: version-one diff --git a/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.2/kustomization.yaml b/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.2/kustomization.yaml new file mode 100644 index 000000000..3ee92e092 --- /dev/null +++ b/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.2/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - version.yaml \ No newline at end of file diff --git a/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.2/version.yaml b/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.2/version.yaml new file mode 100644 index 000000000..6ea271d27 --- /dev/null +++ b/pkg/clusterctl/client/testdata/functions/capi/infrastructure/v0.3.2/version.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: version-two