airshipctl/pkg/phase/executors/clusterctl.go
Ruslan Aliev 136aa06117 Improve memory performance of clusterctl KRM
Switching components type from []byte to string significantly
improves memory effiency of config function.

Change-Id: I81ddf469bf5abd214ff3785915d82ab494af6268
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
2021-10-13 13:26:24 -05:00

352 lines
9.7 KiB
Go

/*
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 executors
import (
"bytes"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"sigs.k8s.io/kustomize/kyaml/yaml"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/cluster/clustermap"
"opendev.org/airship/airshipctl/pkg/container"
"opendev.org/airship/airshipctl/pkg/document"
airerrors "opendev.org/airship/airshipctl/pkg/errors"
"opendev.org/airship/airshipctl/pkg/events"
"opendev.org/airship/airshipctl/pkg/k8s/kubeconfig"
"opendev.org/airship/airshipctl/pkg/log"
phaseerrors "opendev.org/airship/airshipctl/pkg/phase/errors"
"opendev.org/airship/airshipctl/pkg/phase/executors/errors"
"opendev.org/airship/airshipctl/pkg/phase/ifc"
)
const clusterAPIOverrides = "/workdir/.cluster-api/overrides"
var _ ifc.Executor = &ClusterctlExecutor{}
// ClusterctlExecutor phase executor
type ClusterctlExecutor struct {
clusterName string
targetPath string
clusterMap clustermap.ClusterMap
options *airshipv1.Clusterctl
kubecfg kubeconfig.Interface
execObj *airshipv1.GenericContainer
clientFunc container.ClientV1Alpha1FactoryFunc
cctlOpts *airshipv1.ClusterctlOptions
}
var typeMap = map[string]string{
airshipv1.BootstrapProviderType: "bootstrap",
airshipv1.ControlPlaneProviderType: "control-plane",
airshipv1.InfrastructureProviderType: "infrastructure",
airshipv1.CoreProviderType: "core",
}
// NewClusterctlExecutor creates instance of 'clusterctl' phase executor
func NewClusterctlExecutor(cfg ifc.ExecutorConfig) (ifc.Executor, error) {
options := airshipv1.DefaultClusterctl()
if err := cfg.ExecutorDocument.ToAPIObject(options, airshipv1.Scheme); err != nil {
return nil, err
}
cctlOpts := &airshipv1.ClusterctlOptions{
Components: map[string]string{},
}
if err := initRepoData(options, cctlOpts, cfg.TargetPath); err != nil {
return nil, err
}
doc, err := cfg.PhaseConfigBundle.SelectOne(document.NewClusterctlContainerExecutorSelector())
if err != nil {
return nil, err
}
apiObj := airshipv1.DefaultGenericContainer()
err = doc.ToAPIObject(apiObj, airshipv1.Scheme)
if err != nil {
return nil, err
}
clientFunc := container.NewClientV1Alpha1
if cfg.ContainerFunc != nil {
clientFunc = cfg.ContainerFunc
}
return &ClusterctlExecutor{
clusterName: cfg.ClusterName,
options: options,
cctlOpts: cctlOpts,
kubecfg: cfg.KubeConfig,
clusterMap: cfg.ClusterMap,
targetPath: cfg.TargetPath,
execObj: apiObj,
clientFunc: clientFunc,
}, nil
}
func initRepoData(c *airshipv1.Clusterctl, o *airshipv1.ClusterctlOptions, targetPath string) error {
for _, prv := range c.Providers {
rURL, err := url.Parse(prv.URL)
if err != nil {
return err
}
if rURL.Scheme != "" || filepath.IsAbs(prv.URL) {
continue
}
componentDir := filepath.Join(clusterAPIOverrides,
fmt.Sprintf("%s-%s", typeMap[prv.Type], prv.Name), filepath.Base(prv.URL))
if prv.Type == airshipv1.CoreProviderType {
componentDir = filepath.Join(clusterAPIOverrides, prv.Name, filepath.Base(prv.URL))
}
kustomizePath := filepath.Join(targetPath, prv.URL)
log.Debugf("Building cluster-api provider component documents from kustomize path at '%s'", kustomizePath)
bundle, err := document.NewBundleByPath(kustomizePath)
if err != nil {
return err
}
doc, err := bundle.SelectOne(document.NewClusterctlMetadataSelector())
if err != nil {
return err
}
metadata, err := doc.AsYAML()
if err != nil {
return err
}
o.Components[filepath.Join(componentDir, "metadata.yaml")] = string(metadata)
filteredBundle, err := bundle.SelectBundle(document.NewDeployToK8sSelector())
if err != nil {
return err
}
buffer := &bytes.Buffer{}
if err = filteredBundle.Write(buffer); err != nil {
return err
}
prv.URL = filepath.Join(componentDir, fmt.Sprintf("%s-components.yaml", typeMap[prv.Type]))
o.Components[prv.URL] = string(buffer.Bytes())
}
return nil
}
// Run clusterctl init as a phase runner
func (c *ClusterctlExecutor) Run(evtCh chan events.Event, opts ifc.RunOptions) {
defer close(evtCh)
if log.DebugEnabled() {
c.cctlOpts.CmdOptions = append(c.cctlOpts.CmdOptions, "-v5")
}
cctlConfig := map[string]interface{}{
"providers": c.options.Providers,
"images": c.options.ImageMetas,
}
for k, v := range c.options.AdditionalComponentVariables {
cctlConfig[k] = v
}
var err error
c.cctlOpts.Config, err = yaml.Marshal(cctlConfig)
if err != nil {
handleError(evtCh, err)
}
switch c.options.Action {
case airshipv1.Init:
c.init(evtCh)
case airshipv1.Move:
c.move(opts.DryRun, evtCh)
default:
handleError(evtCh, errors.ErrUnknownExecutorAction{Action: string(c.options.Action), ExecutorName: "clusterctl"})
}
}
func (c *ClusterctlExecutor) run() error {
opts, err := yaml.Marshal(c.cctlOpts)
if err != nil {
return err
}
c.execObj.Config = string(opts)
return c.clientFunc("", &bytes.Buffer{}, os.Stdout, c.execObj, c.targetPath).Run()
}
func (c *ClusterctlExecutor) getKubeconfig() (string, string, func(), error) {
kubeConfigFile, cleanup, err := c.kubecfg.GetFile()
if err != nil {
return "", "", nil, err
}
context, err := c.clusterMap.ClusterKubeconfigContext(c.clusterName)
if err != nil {
cleanup()
return "", "", nil, err
}
c.execObj.Spec.StorageMounts = append(c.execObj.Spec.StorageMounts, airshipv1.StorageMount{
MountType: "bind",
Src: kubeConfigFile,
DstPath: kubeConfigFile,
ReadWriteMode: false,
})
return kubeConfigFile, context, cleanup, nil
}
func (c *ClusterctlExecutor) init(evtCh chan events.Event) {
evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
Operation: events.ClusterctlInitStart,
Message: "starting clusterctl init executor",
})
kubecfg, context, cleanup, err := c.getKubeconfig()
if err != nil {
handleError(evtCh, err)
return
}
defer cleanup()
c.cctlOpts.CmdOptions = append(c.cctlOpts.CmdOptions,
"init",
"--kubeconfig", kubecfg,
"--kubeconfig-context", context,
)
initMap := map[string]string{
typeMap[airshipv1.BootstrapProviderType]: c.options.InitOptions.BootstrapProviders,
typeMap[airshipv1.ControlPlaneProviderType]: c.options.InitOptions.ControlPlaneProviders,
typeMap[airshipv1.InfrastructureProviderType]: c.options.InitOptions.InfrastructureProviders,
typeMap[airshipv1.CoreProviderType]: c.options.InitOptions.CoreProvider,
}
for k, v := range initMap {
if v != "" {
c.cctlOpts.CmdOptions = append(c.cctlOpts.CmdOptions, fmt.Sprintf("--%s=%s", k, v))
}
}
if err = c.run(); err != nil {
handleError(evtCh, err)
return
}
eventMsg := "clusterctl init completed successfully"
evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
Operation: events.ClusterctlInitEnd,
Message: eventMsg,
})
}
func (c *ClusterctlExecutor) move(dryRun bool, evtCh chan events.Event) {
evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
Operation: events.ClusterctlMoveStart,
Message: "starting clusterctl move executor",
})
kubecfg, context, cleanup, err := c.getKubeconfig()
if err != nil {
handleError(evtCh, err)
return
}
defer cleanup()
fromCluster, err := c.clusterMap.ParentCluster(c.clusterName)
if err != nil {
handleError(evtCh, err)
return
}
fromContext, err := c.clusterMap.ClusterKubeconfigContext(fromCluster)
if err != nil {
handleError(evtCh, err)
return
}
c.cctlOpts.CmdOptions = append(
c.cctlOpts.CmdOptions,
"move",
"--kubeconfig", kubecfg,
"--kubeconfig-context", fromContext,
"--to-kubeconfig", kubecfg,
"--to-kubeconfig-context", context,
"--namespace", c.options.MoveOptions.Namespace,
)
if dryRun {
c.cctlOpts.CmdOptions = append(
c.cctlOpts.CmdOptions,
"--dry-run",
)
}
if err = c.run(); err != nil {
handleError(evtCh, err)
return
}
eventMsg := "clusterctl move completed successfully"
evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
Operation: events.ClusterctlMoveEnd,
Message: eventMsg,
})
}
// Validate executor configuration and documents
func (c *ClusterctlExecutor) Validate() error {
switch c.options.Action {
case "":
return phaseerrors.ErrInvalidPhase{Reason: "ClusterctlExecutor.Action is empty"}
case airshipv1.Init:
if c.options.InitOptions.CoreProvider == "" {
log.Printf("ClusterctlExecutor.InitOptions.CoreProvider is empty")
}
case airshipv1.Move:
default:
return errors.ErrUnknownExecutorAction{Action: string(c.options.Action)}
}
// TODO: need to find if any other validation needs to be added
return nil
}
// Render executor documents
func (c *ClusterctlExecutor) Render(w io.Writer, ro ifc.RenderOptions) error {
dataAll := &bytes.Buffer{}
for path, data := range c.cctlOpts.Components {
if strings.Contains(path, "components.yaml") {
dataAll.Write(append([]byte(data), []byte("\n---\n")...))
}
}
bundle, err := document.NewBundleFromBytes(dataAll.Bytes())
if err != nil {
return err
}
filtered, err := bundle.SelectBundle(ro.FilterSelector)
if err != nil {
return err
}
return filtered.Write(w)
}
// Status returns the status of the given phase
func (c *ClusterctlExecutor) Status() (ifc.ExecutorStatus, error) {
return ifc.ExecutorStatus{}, airerrors.ErrNotImplemented{What: Clusterctl}
}