diff --git a/cmd/clusterctl/client/config/variables_client_test.go b/cmd/clusterctl/client/config/variables_client_test.go index dc45d638f571..c8a973970da8 100644 --- a/cmd/clusterctl/client/config/variables_client_test.go +++ b/cmd/clusterctl/client/config/variables_client_test.go @@ -47,11 +47,10 @@ func Test_variables_Get(t *testing.T) { wantErr: false, }, { - name: "Returns error if the variable does exists", + name: "Returns error if the variable does not exist", args: args{ key: "baz", }, - want: "", wantErr: true, }, } diff --git a/cmd/clusterctl/client/repository/client.go b/cmd/clusterctl/client/repository/client.go index ca2c19bf3437..80d7104f1e6f 100644 --- a/cmd/clusterctl/client/repository/client.go +++ b/cmd/clusterctl/client/repository/client.go @@ -17,13 +17,9 @@ limitations under the License. package repository import ( - "io/ioutil" "net/url" - "os" - "path/filepath" "github.com/pkg/errors" - "k8s.io/client-go/util/homedir" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" ) @@ -72,7 +68,7 @@ func (c *repositoryClient) Templates(version string) TemplateClient { } func (c *repositoryClient) Metadata(version string) MetadataClient { - return newMetadataClient(c.Provider, version, c.repository) + return newMetadataClient(c.Provider, version, c.repository, c.configClient.Variables()) } // Option is a configuration option supplied to New @@ -168,31 +164,3 @@ func repositoryFactory(providerConfig config.Provider, configVariablesClient con return nil, errors.Errorf("invalid provider url. there are no provider implementation for %q schema", rURL.Scheme) } - -const overrideFolder = "overrides" - -// getLocalOverride return local override file from the config folder, if it exists. -// This is required for development purposes, but it can be used also in production as a workaround for problems on the official repositories -func getLocalOverride(provider config.Provider, version, path string) ([]byte, error) { - // local override files are searched at $home/.cluster-api/overrides/// - homeFolder := filepath.Join(homedir.HomeDir(), config.ConfigFolder) - overridePath := filepath.Join(homeFolder, overrideFolder, provider.ManifestLabel(), version, path) - - // it the local override exists, use it - _, err := os.Stat(overridePath) - if err == nil { - content, err := ioutil.ReadFile(overridePath) - if err != nil { - return nil, errors.Wrapf(err, "failed to read local override for %s/%s/%s", provider.ManifestLabel(), version, path) - } - return content, nil - } - - // it the local override does not exists, return (so files from the provider's repository could be used) - if os.IsNotExist(err) { - return nil, nil - } - - // blocks for any other error - return nil, err -} diff --git a/cmd/clusterctl/client/repository/components_client.go b/cmd/clusterctl/client/repository/components_client.go index 6bab8df0ba08..0e8179b253a0 100644 --- a/cmd/clusterctl/client/repository/components_client.go +++ b/cmd/clusterctl/client/repository/components_client.go @@ -59,7 +59,12 @@ func (f *componentsClient) Get(version, targetNamespace, watchingNamespace strin path := f.repository.ComponentsPath() // read the component YAML, reading the local override file if it exists, otherwise read from the provider repository - file, err := getLocalOverride(f.provider, version, path) + file, err := getLocalOverride(&newOverrideInput{ + configVariablesClient: f.configClient.Variables(), + provider: f.provider, + version: version, + filePath: path, + }) if err != nil { return nil, err } diff --git a/cmd/clusterctl/client/repository/metadata_client.go b/cmd/clusterctl/client/repository/metadata_client.go index f47fc3b2e37a..58ee53395fde 100644 --- a/cmd/clusterctl/client/repository/metadata_client.go +++ b/cmd/clusterctl/client/repository/metadata_client.go @@ -36,20 +36,22 @@ type MetadataClient interface { // metadataClient implements MetadataClient. type metadataClient struct { - provider config.Provider - version string - repository Repository + configVarClient config.VariablesClient + provider config.Provider + version string + repository Repository } // ensure metadataClient implements MetadataClient. var _ MetadataClient = &metadataClient{} // newMetadataClient returns a metadataClient. -func newMetadataClient(provider config.Provider, version string, repository Repository) *metadataClient { +func newMetadataClient(provider config.Provider, version string, repository Repository, config config.VariablesClient) *metadataClient { return &metadataClient{ - provider: provider, - version: version, - repository: repository, + configVarClient: config, + provider: provider, + version: version, + repository: repository, } } @@ -60,7 +62,12 @@ func (f *metadataClient) Get() (*clusterctlv1.Metadata, error) { version := f.version name := "metadata.yaml" - file, err := getLocalOverride(f.provider, version, name) + file, err := getLocalOverride(&newOverrideInput{ + configVariablesClient: f.configVarClient, + provider: f.provider, + version: version, + filePath: name, + }) if err != nil { return nil, err } diff --git a/cmd/clusterctl/client/repository/metadata_client_test.go b/cmd/clusterctl/client/repository/metadata_client_test.go index abc75d96a024..347a1a121367 100644 --- a/cmd/clusterctl/client/repository/metadata_client_test.go +++ b/cmd/clusterctl/client/repository/metadata_client_test.go @@ -137,9 +137,10 @@ func Test_metadataClient_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := &metadataClient{ - provider: tt.fields.provider, - version: tt.fields.version, - repository: tt.fields.repository, + configVarClient: test.NewFakeVariableClient(), + provider: tt.fields.provider, + version: tt.fields.version, + repository: tt.fields.repository, } got, err := f.Get() if tt.wantErr { diff --git a/cmd/clusterctl/client/repository/overrides.go b/cmd/clusterctl/client/repository/overrides.go new file mode 100644 index 000000000000..b2ae2485ac92 --- /dev/null +++ b/cmd/clusterctl/client/repository/overrides.go @@ -0,0 +1,103 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 + + http://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 repository + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "k8s.io/client-go/util/homedir" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" +) + +const ( + overrideFolder = "overrides" + overrideFolderKey = "overridesFolder" +) + +// Overrider provides behavior to determine the overrides layer. +type Overrider interface { + Path() string +} + +// overrides implements the Overrider interface. +type overrides struct { + configVariablesClient config.VariablesClient + providerLabel string + version string + filePath string +} + +type newOverrideInput struct { + configVariablesClient config.VariablesClient + provider config.Provider + version string + filePath string +} + +// newOverride returns an Overrider. +func newOverride(o *newOverrideInput) Overrider { + return &overrides{ + configVariablesClient: o.configVariablesClient, + providerLabel: o.provider.ManifestLabel(), + version: o.version, + filePath: o.filePath, + } +} + +// Path returns the fully formed path to the file within the specified +// overrides config. +func (o *overrides) Path() string { + basepath := filepath.Join(homedir.HomeDir(), config.ConfigFolder, overrideFolder) + f, err := o.configVariablesClient.Get(overrideFolderKey) + if err == nil && len(strings.TrimSpace(f)) != 0 { + basepath = f + } + + return filepath.Join( + basepath, + o.providerLabel, + o.version, + o.filePath, + ) +} + +// getLocalOverride return local override file from the config folder, if it exists. +// This is required for development purposes, but it can be used also in production as a workaround for problems on the official repositories +func getLocalOverride(info *newOverrideInput) ([]byte, error) { + overridePath := newOverride(info).Path() + // it the local override exists, use it + _, err := os.Stat(overridePath) + if err == nil { + content, err := ioutil.ReadFile(overridePath) + if err != nil { + return nil, errors.Wrapf(err, "failed to read local override for %s", overridePath) + } + return content, nil + } + + // it the local override does not exists, return (so files from the provider's repository could be used) + if os.IsNotExist(err) { + return nil, nil + } + + // blocks for any other error + return nil, err +} diff --git a/cmd/clusterctl/client/repository/overrides_test.go b/cmd/clusterctl/client/repository/overrides_test.go new file mode 100644 index 000000000000..2cf5efe5d1af --- /dev/null +++ b/cmd/clusterctl/client/repository/overrides_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 + + http://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 repository + +import ( + "os" + "path/filepath" + "testing" + + . "github.com/onsi/gomega" + "k8s.io/client-go/util/homedir" + clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" +) + +func TestOverrides(t *testing.T) { + tests := []struct { + name string + configVarClient config.VariablesClient + expectedPath string + }{ + { + name: "returns default overrides path if no config provided", + configVarClient: test.NewFakeVariableClient(), + expectedPath: filepath.Join(homedir.HomeDir(), config.ConfigFolder, overrideFolder, "infrastructure-myinfra", "v1.0.1", "infra-comp.yaml"), + }, + { + name: "returns default overrides path if config variable is empty", + configVarClient: test.NewFakeVariableClient().WithVar(overrideFolderKey, ""), + expectedPath: filepath.Join(homedir.HomeDir(), config.ConfigFolder, overrideFolder, "infrastructure-myinfra", "v1.0.1", "infra-comp.yaml"), + }, + { + name: "returns default overrides path if config variable is whitespace", + configVarClient: test.NewFakeVariableClient().WithVar(overrideFolderKey, " "), + expectedPath: filepath.Join(homedir.HomeDir(), config.ConfigFolder, overrideFolder, "infrastructure-myinfra", "v1.0.1", "infra-comp.yaml"), + }, + { + name: "uses overrides folder from the config variables", + configVarClient: test.NewFakeVariableClient().WithVar(overrideFolderKey, "/Users/foobar/workspace/releases"), + expectedPath: "/Users/foobar/workspace/releases/infrastructure-myinfra/v1.0.1/infra-comp.yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + provider := config.NewProvider("myinfra", "", clusterctlv1.InfrastructureProviderType) + override := newOverride(&newOverrideInput{ + configVariablesClient: tt.configVarClient, + provider: provider, + version: "v1.0.1", + filePath: "infra-comp.yaml", + }) + + g.Expect(override.Path()).To(Equal(tt.expectedPath)) + }) + } +} + +func TestGetLocalOverrides(t *testing.T) { + t.Run("returns contents of file successfully", func(t *testing.T) { + g := NewWithT(t) + tmpDir := createTempDir(t) + defer os.RemoveAll(tmpDir) + + createLocalTestProviderFile(t, tmpDir, "infrastructure-myinfra/v1.0.1/infra-comp.yaml", "foo: bar") + + info := &newOverrideInput{ + configVariablesClient: test.NewFakeVariableClient().WithVar(overrideFolderKey, tmpDir), + provider: config.NewProvider("myinfra", "", clusterctlv1.InfrastructureProviderType), + version: "v1.0.1", + filePath: "infra-comp.yaml", + } + + b, err := getLocalOverride(info) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(string(b)).To(Equal("foo: bar")) + }) + + t.Run("doesn't return error if file does not exist", func(t *testing.T) { + g := NewWithT(t) + + info := &newOverrideInput{ + configVariablesClient: test.NewFakeVariableClient().WithVar(overrideFolderKey, "do-not-exist"), + provider: config.NewProvider("myinfra", "", clusterctlv1.InfrastructureProviderType), + version: "v1.0.1", + filePath: "infra-comp.yaml", + } + + _, err := getLocalOverride(info) + g.Expect(err).ToNot(HaveOccurred()) + }) +} diff --git a/cmd/clusterctl/client/repository/template_client.go b/cmd/clusterctl/client/repository/template_client.go index 349c3db72554..5352c2cb8582 100644 --- a/cmd/clusterctl/client/repository/template_client.go +++ b/cmd/clusterctl/client/repository/template_client.go @@ -85,7 +85,12 @@ func (c *templateClient) Get(flavor, targetNamespace string, listVariablesOnly b name = fmt.Sprintf("%s.yaml", name) // read the component YAML, reading the local override file if it exists, otherwise read from the provider repository - rawYaml, err := getLocalOverride(c.provider, version, name) + rawYaml, err := getLocalOverride(&newOverrideInput{ + configVariablesClient: c.configVariablesClient, + provider: c.provider, + version: version, + filePath: name, + }) if err != nil { return nil, err } diff --git a/cmd/clusterctl/test/run-e2e.sh b/cmd/clusterctl/test/run-e2e.sh index 0c33134c4301..5c74f1d7f2a8 100755 --- a/cmd/clusterctl/test/run-e2e.sh +++ b/cmd/clusterctl/test/run-e2e.sh @@ -46,21 +46,23 @@ cat < "clusterctl-settings.json" EOF # Create a local filesystem repository for the docker provider and update clusterctl.yaml -LOCAL_CAPD_REPO_PATH="${ARTIFACTS}/testdata/docker" +LOCAL_CAPD_REPO_PATH="${ARTIFACTS}/testdata/infrastructure-docker" mkdir -p "${LOCAL_CAPD_REPO_PATH}" cp -r "${REPO_ROOT_ABS}/cmd/clusterctl/test/testdata/docker/${CAPD_VERSION}" "${LOCAL_CAPD_REPO_PATH}" # We build the infrastructure-components.yaml from the capd folder and put in local repo folder kustomize build "${REPO_ROOT_ABS}/test/infrastructure/docker/config/default/" > "${LOCAL_CAPD_REPO_PATH}/${CAPD_VERSION}/infrastructure-components.yaml" -export CLUSTERCTL_CONFIG="${ARTIFACTS}/testdata/clusterctl.yaml" +export CLUSTERCTL_CONFIG="${ARTIFACTS}/testdata/clusterctl.yaml" cat < "${CLUSTERCTL_CONFIG}" providers: - name: docker url: ${LOCAL_CAPD_REPO_PATH}/${CAPD_VERSION}/infrastructure-components.yaml type: InfrastructureProvider +overridesFolder:${ARTIFACTS}/testdata + DOCKER_SERVICE_DOMAIN: "cluster.local" -DOCKER_SERVICE_CIDRS: "10.128.0.0/12" -DOCKER_POD_CIDRS: "192.168.0.0/16" +DOCKER_SERVICE_CIDRS: "10.128.0.0/12" +DOCKER_POD_CIDRS: "192.168.0.0/16" EOF export KIND_CONFIG_FILE="${ARTIFACTS}/kind-cluster-with-extramounts.yaml" diff --git a/docs/book/src/clusterctl/configuration.md b/docs/book/src/clusterctl/configuration.md index b05165dc14c3..8c640484c413 100644 --- a/docs/book/src/clusterctl/configuration.md +++ b/docs/book/src/clusterctl/configuration.md @@ -10,7 +10,7 @@ The `clusterctl` config file is located at `$HOME/.cluster-api/clusterctl.yaml` The `clusterctl` CLI is designed to work with providers implementing the [clusterctl Provider Contract](provider-contract.md). -Each provider is expected to define a provider repository, a well-known place where release assets are published. +Each provider is expected to define a provider repository, a well-known place where release assets are published. By default, `clusterctl` ships with providers sponsored by SIG Cluster Lifecycle. Use `clusterctl config repositories` to get a list of supported @@ -35,9 +35,9 @@ See [provider contract](provider-contract.md) for instructions about how to set ## Variables When installing a provider `clusterctl` reads a YAML file that is published in the provider repository; while executing -this operation, `clusterctl` can substitute certain variables with the ones provided by the user. +this operation, `clusterctl` can substitute certain variables with the ones provided by the user. -The same mechanism also applies when `clusterctl` reads the cluster templates YAML published in the repository, e.g. +The same mechanism also applies when `clusterctl` reads the cluster templates YAML published in the repository, e.g. when injecting the Kubernetes version to use, or the number of worker machines to create. The user can provide values using OS environment variables, but it is also possible to add @@ -50,6 +50,73 @@ AWS_B64ENCODED_CREDENTIALS: XXXXXXXX In case a variable is defined both in the config file and as an OS environment variable, the latter takes precedence. +## Overrides Layer + +`clusterctl` uses an overrides layer to read in injected provider components, +cluster templates and metadata. By default, it reads the files from +`$HOME/.cluster-api/overrides`. + +The directory structure under the `overrides` directory should follow the +template +``` +// +``` +For example, +``` +├── bootstrap-kubeadm +│   └── v0.3.0 +│   └── bootstrap-components.yaml +├── cluster-api +│   └── v0.3.0 +│   └── core-components.yaml +├── control-plane-kubeadm +│   └── v0.3.0 +│   └── control-plane-components.yaml +└── infrastructure-aws + └── v0.5.0 + ├── cluster-template-dev.yaml + └── infrastructure-components.yaml +``` + +For developers who want to generate the overrides layer, see [Run the +local-overrides hack!](developers.md#run-the-local-overrides-hack). + +Once these overrides are specified, `clusterctl` will use them instead of +getting the values from the default or specified providers. + +One example usage of the overrides layer is that it allows you to deploy +clusters with custom templates that may not be available from the official +provider repositories. +For example, you can now do +```bash +clusterctl config cluster mycluster --flavor dev --infrastructure aws:v0.5.0 -v5 +``` + +The `-v5` provides verbose logging which will confirm the usage of the +override file. +```bash +Using Override="cluster-template-dev.yaml" Provider="infrastructure-aws" Version="v0.5.0" +``` + +Another example, if you would like to deploy a custom version of CAPA, you can +make changes to `infrastructure-components.yaml` in the overrides folder and +run, +```bash +clusterctl init --infrastructure aws:v0.5.0 -v5 +... +Using Override="infrastructure-components.yaml" Provider="infrastructure-aws" Version="v0.5.0" +... +``` + + +If you prefer to have the overrides directory at a different location (e.g. +`/Users/foobar/workspace/dev-releases`) you can specify the overrides +directory in the clusterctl config file as + +```yaml +overridesFolder: /Users/foobar/workspace/dev-releases +``` + ## Image overrides