diff --git a/pkg/cmd/render/render.go b/pkg/cmd/render/render.go index ec99aeb38..1beaae5be 100644 --- a/pkg/cmd/render/render.go +++ b/pkg/cmd/render/render.go @@ -1,6 +1,7 @@ package render import ( + "fmt" "io/ioutil" "github.com/spf13/cobra" @@ -8,6 +9,7 @@ import ( "k8s.io/klog" configv1 "github.com/openshift/api/config/v1" + kubecloudconfig "github.com/openshift/cluster-config-operator/pkg/operator/kube_cloud_config" genericrender "github.com/openshift/library-go/pkg/operator/render" genericrenderoptions "github.com/openshift/library-go/pkg/operator/render/options" ) @@ -18,6 +20,10 @@ type renderOpts struct { generic genericrenderoptions.GenericOptions clusterConfigFile string + + clusterInfrastructureInputFile string + cloudProviderConfigInputFile string + cloudProviderConfigOutputFile string } // NewRenderCommand creates a render command. @@ -52,6 +58,16 @@ func (r *renderOpts) AddFlags(fs *pflag.FlagSet) { r.generic.AddFlags(fs, configv1.GroupVersion.WithKind("Config")) fs.StringVar(&r.clusterConfigFile, "cluster-config-file", r.clusterConfigFile, "Openshift Cluster API Config file.") + + // This is the file containing the infrastructure object + fs.StringVar(&r.clusterInfrastructureInputFile, "cluster-infrastructure-input-file", r.clusterInfrastructureInputFile, "Input path for the cluster infrastructure file.") + + // This is the file containing the configmap for the kube cloud config provided by the user + fs.StringVar(&r.cloudProviderConfigInputFile, "cloud-provider-config-input-file", r.cloudProviderConfigInputFile, "Input path for the cloud provider config file.") + + // This is the generated kube cloud config + fs.StringVar(&r.cloudProviderConfigOutputFile, "cloud-provider-config-output-file", r.cloudProviderConfigOutputFile, "Output path for the generated cloud provider config file.") + } // Validate verifies the inputs. @@ -63,6 +79,18 @@ func (r *renderOpts) Validate() error { return err } + // Validate all files are specified when specifying infrastructure and configmap files + if infra, provider := len(r.clusterInfrastructureInputFile) != 0, len(r.cloudProviderConfigOutputFile) != 0; infra || provider { + if !(infra && provider) { + return fmt.Errorf("clulster-infrastructure-file and cloud-provider-config-output-file must be specified.") + } + if infra { + if err := kubecloudconfig.ValidateFile(r.clusterInfrastructureInputFile); err != nil { + return err + } + } + } + return nil } @@ -85,6 +113,7 @@ type TemplateData struct { // Run contains the logic of the render command. func (r *renderOpts) Run() error { renderConfig := TemplateData{} + if len(r.clusterConfigFile) > 0 { _, err := ioutil.ReadFile(r.clusterConfigFile) if err != nil { @@ -92,21 +121,33 @@ func (r *renderOpts) Run() error { } // TODO I'm thinking we parse this into a map and reference it that way } + if err := r.manifest.ApplyTo(&renderConfig.ManifestConfig); err != nil { return err } if err := r.generic.ApplyTo( &renderConfig.FileConfig, - - // TODO I don't think we have these genericrenderoptions.Template{}, genericrenderoptions.Template{}, - &renderConfig, nil, ); err != nil { return err } - return genericrender.WriteFiles(&r.generic, &renderConfig.FileConfig, renderConfig) + if err := genericrender.WriteFiles(&r.generic, &renderConfig.FileConfig, renderConfig); err != nil { + return err + } + + if len(r.clusterInfrastructureInputFile) > 0 && len(r.cloudProviderConfigOutputFile) > 0 { + targetCloudConfigMapData, err := kubecloudconfig.BootstrapTransform(r.clusterInfrastructureInputFile, r.cloudProviderConfigInputFile) + if err != nil { + return err + } + if err := ioutil.WriteFile(r.cloudProviderConfigOutputFile, targetCloudConfigMapData, 0644); err != nil { + return fmt.Errorf("failed to write merged config to %q: %v", r.cloudProviderConfigOutputFile, err) + } + } + + return nil } diff --git a/pkg/operator/kube_cloud_config/aws.go b/pkg/operator/kube_cloud_config/aws.go index 10545657b..4649ae201 100644 --- a/pkg/operator/kube_cloud_config/aws.go +++ b/pkg/operator/kube_cloud_config/aws.go @@ -1,4 +1,4 @@ -package kube_cloud_config +package kubecloudconfig import ( "bytes" diff --git a/pkg/operator/kube_cloud_config/aws_test.go b/pkg/operator/kube_cloud_config/aws_test.go index 2ea322f0a..d72169ed5 100644 --- a/pkg/operator/kube_cloud_config/aws_test.go +++ b/pkg/operator/kube_cloud_config/aws_test.go @@ -1,4 +1,4 @@ -package kube_cloud_config +package kubecloudconfig import ( "testing" diff --git a/pkg/operator/kube_cloud_config/azure.go b/pkg/operator/kube_cloud_config/azure.go index d4fafa586..740eccc03 100644 --- a/pkg/operator/kube_cloud_config/azure.go +++ b/pkg/operator/kube_cloud_config/azure.go @@ -1,4 +1,4 @@ -package kube_cloud_config +package kubecloudconfig import ( "bytes" diff --git a/pkg/operator/kube_cloud_config/azure_test.go b/pkg/operator/kube_cloud_config/azure_test.go index 152f7918d..a2d003fb6 100644 --- a/pkg/operator/kube_cloud_config/azure_test.go +++ b/pkg/operator/kube_cloud_config/azure_test.go @@ -1,4 +1,4 @@ -package kube_cloud_config +package kubecloudconfig import ( "testing" diff --git a/pkg/operator/kube_cloud_config/bootstrap.go b/pkg/operator/kube_cloud_config/bootstrap.go new file mode 100644 index 000000000..09f5baa03 --- /dev/null +++ b/pkg/operator/kube_cloud_config/bootstrap.go @@ -0,0 +1,89 @@ +package kubecloudconfig + +import ( + "fmt" + "io/ioutil" + "os" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" + + configv1 "github.com/openshift/api/config/v1" + operatorclient "github.com/openshift/cluster-config-operator/pkg/operator/operatorclient" +) + +// ValidateFile verifies a file exists, has content, and is a regular file +func ValidateFile(path string) error { + st, err := os.Stat(path) + if err != nil { + return fmt.Errorf("failed to stat %s: %w", path, err) + } + if !st.Mode().IsRegular() { + return fmt.Errorf("%s is not a regular file", path) + } + if st.Size() <= 0 { + return fmt.Errorf("%s is empty", path) + } + return nil +} + +// BootstrapTransform implements the cloudConfigTransformer during bootstrapping. +// It uses the input ConfigMap and Infrastructure provided by files on the bootstrap +// host to create a new config that has the cloud field set. +func BootstrapTransform(infrastructureFile string, cloudProviderFile string) ([]byte, error) { + + // Read, parse, and save the infrastructure object + var clusterInfrastructure configv1.Infrastructure + fileData, err := ioutil.ReadFile(infrastructureFile) + if err != nil { + return nil, fmt.Errorf("failed to read infrastructure file: %w", err) + } + err = yaml.Unmarshal(fileData, &clusterInfrastructure) + if err != nil { + return nil, fmt.Errorf("failed unmarshal infrastructure: %w", err) + } + + // Read, parse, and save the user provided cloud configmap + var cloudProviderConfigInput corev1.ConfigMap + if len(cloudProviderFile) > 0 { + fileData, err = ioutil.ReadFile(cloudProviderFile) + if err != nil { + return nil, fmt.Errorf("failed to read cloud provider file: %w", err) + } + err = yaml.Unmarshal(fileData, &cloudProviderConfigInput) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal cloud provider: %w", err) + } + } + + // Determine the platform type + var platformName configv1.PlatformType + if pstatus := clusterInfrastructure.Status.PlatformStatus; pstatus != nil { + platformName = pstatus.Type + } + if len(platformName) == 0 { + platformName = clusterInfrastructure.Status.Platform + } + + // Determine the platform specific transformer method to use + cloudConfigTransformers := cloudConfigTransformers() + cloudConfigTransformerFn, ok := cloudConfigTransformers[platformName] + if !ok { + cloudConfigTransformerFn = asIsTransformer + } + target, err := cloudConfigTransformerFn(&cloudProviderConfigInput, clusterInfrastructure.Spec.CloudConfig.Key, &clusterInfrastructure) + if err != nil { + return nil, fmt.Errorf("failed to transform cloud config: %w", err) + } + + target.Name = targetConfigName + target.Namespace = operatorclient.GlobalMachineSpecifiedConfigNamespace + /* ApplyConfigMap() */ + + targetCloudConfigMapData, err := yaml.Marshal(target) + if err != nil { + return nil, fmt.Errorf("failed to marhsal cloud config: %w", err) + } + + return targetCloudConfigMapData, nil +} diff --git a/pkg/operator/kube_cloud_config/controller.go b/pkg/operator/kube_cloud_config/controller.go index ab01856c8..c56cef1b9 100644 --- a/pkg/operator/kube_cloud_config/controller.go +++ b/pkg/operator/kube_cloud_config/controller.go @@ -1,4 +1,4 @@ -package kube_cloud_config +package kubecloudconfig import ( "context" @@ -49,13 +49,10 @@ func NewController(operatorClient operatorv1helpers.OperatorClient, openshiftConfigConfigMapInformer cache.SharedIndexInformer, openshiftConfigManagedConfigMapInformer cache.SharedIndexInformer, recorder events.Recorder) factory.Controller { c := &KubeCloudConfigController{ - infraClient: infraClient.Infrastructures(), - infraLister: infraLister, - configMapClient: configMapClient, - cloudConfigTransformers: map[configv1.PlatformType]cloudConfigTransformer{ - configv1.AWSPlatformType: awsTransformer, - configv1.AzurePlatformType: azureTransformer, - }, + infraClient: infraClient.Infrastructures(), + infraLister: infraLister, + configMapClient: configMapClient, + cloudConfigTransformers: cloudConfigTransformers(), } return factory.New(). WithInformers( @@ -154,3 +151,12 @@ func asIsTransformer(input *corev1.ConfigMap, sourceKey string, _ *configv1.Infr return output, nil } + +// cloudConfigTransformers returns all configured cloud transformers +func cloudConfigTransformers() map[configv1.PlatformType]cloudConfigTransformer { + cloudConfigTransformers := map[configv1.PlatformType]cloudConfigTransformer{ + configv1.AWSPlatformType: awsTransformer, + configv1.AzurePlatformType: azureTransformer, + } + return cloudConfigTransformers +} diff --git a/pkg/operator/kube_cloud_config/controller_test.go b/pkg/operator/kube_cloud_config/controller_test.go index 9cc38b423..9868917cd 100644 --- a/pkg/operator/kube_cloud_config/controller_test.go +++ b/pkg/operator/kube_cloud_config/controller_test.go @@ -1,4 +1,4 @@ -package kube_cloud_config +package kubecloudconfig import ( "context" @@ -214,13 +214,10 @@ SubnetID = subnet-test fakeConfig := configfakeclient.NewSimpleClientset(test.inputinfra) ctrl := KubeCloudConfigController{ - infraClient: fakeConfig.ConfigV1().Infrastructures(), - infraLister: configv1listers.NewInfrastructureLister(indexerInfra), - configMapClient: fake.CoreV1(), - cloudConfigTransformers: map[configv1.PlatformType]cloudConfigTransformer{ - configv1.AWSPlatformType: awsTransformer, - configv1.AzurePlatformType: azureTransformer, - }, + infraClient: fakeConfig.ConfigV1().Infrastructures(), + infraLister: configv1listers.NewInfrastructureLister(indexerInfra), + configMapClient: fake.CoreV1(), + cloudConfigTransformers: cloudConfigTransformers(), } err := ctrl.sync(context.TODO(), diff --git a/pkg/operator/kube_cloud_config/doc.go b/pkg/operator/kube_cloud_config/doc.go index b89d33eb7..bccf09b4b 100644 --- a/pkg/operator/kube_cloud_config/doc.go +++ b/pkg/operator/kube_cloud_config/doc.go @@ -1,3 +1,3 @@ -// Package kube_cloud_config controller is responsible for stitching the user provided kuberneted cloud configuration file and +// Package kubecloudconfig controller is responsible for stitching the user provided kuberneted cloud configuration file and // the various platform specific settings provided in the infrastructures.config.openshift.io -package kube_cloud_config +package kubecloudconfig diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index d1a99f062..af77c67fe 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -57,7 +57,7 @@ func RunOperator(ctx context.Context, controllerContext *controllercmd.Controlle controllerContext.EventRecorder, ) - kubeCloudConfigController := kube_cloud_config.NewController( + kubeCloudConfigController := kubecloudconfig.NewController( operatorClient, configClient.ConfigV1(), configInformers.Config().V1().Infrastructures().Lister(),