diff --git a/cmd/install/assets/hypershift_operator.go b/cmd/install/assets/hypershift_operator.go index b8454980d85..1587b385184 100644 --- a/cmd/install/assets/hypershift_operator.go +++ b/cmd/install/assets/hypershift_operator.go @@ -377,7 +377,7 @@ type HyperShiftOperatorDeployment struct { ManagedService string EnableSizeTagging bool EnableEtcdRecovery bool - RegistryOverrides string + RegistryOverrides string } func (o HyperShiftOperatorDeployment) Build() *appsv1.Deployment { diff --git a/cmd/install/install.go b/cmd/install/install.go index f761bc7a9b7..03b489656ae 100644 --- a/cmd/install/install.go +++ b/cmd/install/install.go @@ -104,8 +104,8 @@ type Options struct { ManagedService string EnableSizeTagging bool EnableEtcdRecovery bool - RegistryOverrides string - RenderNamespace bool + RegistryOverrides string + RenderNamespace bool } func (o *Options) Validate() error { @@ -688,7 +688,7 @@ func setupOperatorResources(opts Options, userCABundleCM *corev1.ConfigMap, trus ManagedService: opts.ManagedService, EnableSizeTagging: opts.EnableSizeTagging, EnableEtcdRecovery: opts.EnableEtcdRecovery, - RegistryOverrides: opts.RegistryOverrides, + RegistryOverrides: opts.RegistryOverrides, }.Build() operatorService := assets.HyperShiftOperatorService{ Namespace: operatorNamespace, diff --git a/cmd/install/install_helm.go b/cmd/install/install_helm.go index 9a8d91f5099..58e75186ab6 100644 --- a/cmd/install/install_helm.go +++ b/cmd/install/install_helm.go @@ -5,32 +5,31 @@ import ( "os" "strings" + "github.com/openshift/hypershift/pkg/version" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" crclient "sigs.k8s.io/controller-runtime/pkg/client" ) - var helmTemplateParams = TemplateParams{ - HyperShiftImage: ".Values.operator.image", - HyperShiftImageTag: ".Values.operator.imageTag", - Namespace: ".Release.Namespace", - OIDCS3Name: ".Values.oidc.s3.name", - OIDCS3Region: ".Values.oidc.s3.region", - OIDCS3CredsSecret: ".Values.oidc.s3.credsSecret", - OIDCS3CredsSecretKey: ".Values.oidc.s3.credsSecretKey", - AWSPrivateRegion: ".Values.aws.private.region", - AWSPrivateCredsSecret: ".Values.aws.private.credsSecret", - AWSPrivateCredsSecretKey: ".Values.aws.private.credsSecretKey", - ExternalDNSCredsSecret: ".Values.externaldns.credsSecret", - ExternalDNSDomainFilter: ".Values.externaldns.domainFilter", - ExternalDNSTxtOwnerID: ".Values.externaldns.txtOwnerId", - ExternalDNSAzureWorkloadIdentity: ".Values.externaldns.azureWorkloadIdentity", - ExternalDNSImage: ".Values.externaldns.image", - RegistryOverrides: "registryOverrides", - TemplateNamespace: false, + Namespace: ".Release.Namespace", + HyperShiftImage: ".Values.image", + OIDCS3Name: ".Values.oidc.s3.name", + OIDCS3Region: ".Values.oidc.s3.region", + OIDCS3CredsSecret: ".Values.oidc.s3.credsSecret", + OIDCS3CredsSecretKey: ".Values.oidc.s3.credsSecretKey", + AWSPrivateRegion: ".Values.aws.private.region", + AWSPrivateCredsSecret: ".Values.aws.private.credsSecret", + AWSPrivateCredsSecretKey: ".Values.aws.private.credsSecretKey", + ExternalDNSCredsSecret: ".Values.externaldns.credsSecret", + ExternalDNSDomainFilter: ".Values.externaldns.domainFilter", + ExternalDNSTxtOwnerID: ".Values.externaldns.txtOwnerId", + ExternalDNSImage: ".Values.externaldns.image", + RegistryOverrides: ".Values.registryOverrides", + TemplateNamespace: false, TemplateParamWrapper: func(name string) string { return fmt.Sprintf("{{ %s }}", name) - }, + }, } func NewHelmRenderCommand(opts *Options) *cobra.Command { @@ -53,15 +52,87 @@ func NewHelmRenderCommand(opts *Options) *cobra.Command { if opts.OutputFile == "" { opts.OutputFile = "./chart" } - WriteManifestsToDir(crds, fmt.Sprintf("%s/crds", opts.OutputFile)) - WriteManifestsToDir(manifests, fmt.Sprintf("%s/templates", opts.OutputFile)) + err = writeManifestsToDir(crds, fmt.Sprintf("%s/crds", opts.OutputFile)) + if err != nil { + return err + } + err = writeManifestsToDir(manifests, fmt.Sprintf("%s/templates", opts.OutputFile)) + if err != nil { + return err + } + err = WriteChartYaml(opts.OutputFile) + if err != nil { + return err + } + err = WriteValuesFile(opts.OutputFile) + if err != nil { + return err + } return nil } return cmd } -func WriteManifestsToDir(manifests []crclient.Object, dir string) error { +func WriteChartYaml(dir string) error { + data := map[string]interface{}{ + "apiVersion": "v2", + "name": "hypershift-operator", + "description": "A Helm chart for the HyperShift Operator", + "type": "application", + "version": "0.1.0", + "appVersion": version.GetRevision(), + } + return writeYamlFile(fmt.Sprintf("%s/Chart.yaml", dir), data) +} + +func WriteValuesFile(dir string) error { + data := map[string]interface{}{ + "image": "", + "registryOverrides": "", + "oidc": map[string]interface{}{ + "s3": map[string]interface{}{ + "name": "", + "region": "", + "credsSecret": "", + "credsSecretKey": "", + }, + }, + "aws": map[string]interface{}{ + "private": map[string]interface{}{ + "region": "", + "credsSecret": "", + "credsSecretKey": "", + }, + }, + "externaldns": map[string]interface{}{ + "credsSecret": "", + "domainFilter": "", + "txtOwnerId": "", + "image": "", + }, + } + return writeYamlFile(fmt.Sprintf("%s/values.yaml", dir), data) +} + +func writeYamlFile(path string, data map[string]interface{}) error { + yamlData, err := yaml.Marshal(data) + if err != nil { + return err + } + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + _, err = file.Write(yamlData) + if err != nil { + return err + } + return nil +} + +func writeManifestsToDir(manifests []crclient.Object, dir string) error { err := os.MkdirAll(dir, os.ModePerm) if err != nil { return err diff --git a/cmd/install/install_helm_test.go b/cmd/install/install_helm_test.go new file mode 100644 index 00000000000..00761b1fcad --- /dev/null +++ b/cmd/install/install_helm_test.go @@ -0,0 +1,70 @@ +package install + +import ( + "bytes" + "io" + "os" + "testing" +) + +func ExecuteTestHelmCommand(args []string) ([]byte, error) { + // append helm to args + args = append([]string{"helm"}, args...) + cmd := NewCommand() + cmd.SetArgs(args) + b := bytes.NewBufferString("") + cmd.SetOut(b) + err := cmd.Execute() + if err != nil { + return []byte{}, err + } + return io.ReadAll(b) +} + +func TestHelmCommand(t *testing.T) { + // create a folder to hold test data and delete it afterwards + tmpDir, err := os.MkdirTemp("", "test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + _, err = ExecuteTestHelmCommand([]string{"--output-dir", tmpDir}) + if err != nil { + t.Fatal(err) + } + + // check if the output directory exists + if _, err := os.Stat(tmpDir); os.IsNotExist(err) { + t.Fatalf("Output directory %s does not exist", tmpDir) + } + + // check if the crds directory exists ... + for _, dir := range []string{"crds", "templates"} { + dirPath := tmpDir + "/" + dir + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + t.Fatalf("%s directory %s does not exist", dir, dirPath) + } + files, err := os.ReadDir(dirPath) + if err != nil { + t.Fatal(err) + } + if len(files) == 0 { + t.Fatalf("%s directory is empty", dirPath) + } + } + + // check if the Chart.yaml file exists + chartPath := tmpDir + "/Chart.yaml" + if _, err := os.Stat(chartPath); os.IsNotExist(err) { + t.Fatalf("Chart.yaml file %s does not exist", chartPath) + } + + // check if the values.yaml file exists + valuesPath := tmpDir + "/values.yaml" + if _, err := os.Stat(valuesPath); os.IsNotExist(err) { + t.Fatalf("values.yaml file %s does not exist", valuesPath) + } + + +} diff --git a/cmd/install/install_render.go b/cmd/install/install_render.go index 4af1f87d28c..12656bd42de 100644 --- a/cmd/install/install_render.go +++ b/cmd/install/install_render.go @@ -9,6 +9,7 @@ import ( hyperapi "github.com/openshift/hypershift/support/api" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" crclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -27,26 +28,25 @@ var ( ) var openshiftTemplateParams = TemplateParams{ - HyperShiftImage: "OPERATOR_IMG", - HyperShiftImageTag: "IMAGE_TAG", - Namespace: "NAMESPACE", - OIDCS3Name: "OIDC_S3_NAME", - OIDCS3Region: "OIDC_S3_REGION", - OIDCS3CredsSecret: "OIDC_S3_CREDS_SECRET", - OIDCS3CredsSecretKey: "OIDC_S3_CREDS_SECRET_KEY", - AWSPrivateRegion: "AWS_PRIVATE_REGION", - AWSPrivateCredsSecret: "AWS_PRIVATE_CREDS_SECRET", - AWSPrivateCredsSecretKey: "AWS_PRIVATE_CREDS_SECRET_KEY", - ExternalDNSCredsSecret: "EXTERNAL_DNS_CREDS_SECRET", - ExternalDNSDomainFilter: "EXTERNAL_DNS_DOMAIN_FILTER", - ExternalDNSTxtOwnerID: "EXTERNAL_DNS_TXT_OWNER_ID", - ExternalDNSAzureWorkloadIdentity: "EXTERNAL_DNS_AZURE_WORKLOAD_IDENTITY", - ExternalDNSImage: "EXTERNAL_DNS_IMAGE", - RegistryOverrides: "REGISTRY_OVERRIDES", - TemplateNamespace: true, + HyperShiftImage: "OPERATOR_IMG", + Namespace: "NAMESPACE", + HypershiftOperatorReplicas: "OPERATOR_REPLICAS", + OIDCS3Name: "OIDC_S3_NAME", + OIDCS3Region: "OIDC_S3_REGION", + OIDCS3CredsSecret: "OIDC_S3_CREDS_SECRET", + OIDCS3CredsSecretKey: "OIDC_S3_CREDS_SECRET_KEY", + AWSPrivateRegion: "AWS_PRIVATE_REGION", + AWSPrivateCredsSecret: "AWS_PRIVATE_CREDS_SECRET", + AWSPrivateCredsSecretKey: "AWS_PRIVATE_CREDS_SECRET_KEY", + ExternalDNSCredsSecret: "EXTERNAL_DNS_CREDS_SECRET", + ExternalDNSDomainFilter: "EXTERNAL_DNS_DOMAIN_FILTER", + ExternalDNSTxtOwnerID: "EXTERNAL_DNS_TXT_OWNER_ID", + ExternalDNSImage: "EXTERNAL_DNS_IMAGE", + RegistryOverrides: "REGISTRY_OVERRIDES", + TemplateNamespace: true, TemplateParamWrapper: func(name string) string { return fmt.Sprintf("${%s}", name) - }, + }, } func NewRenderCommand(opts *Options) *cobra.Command { @@ -138,10 +138,12 @@ func openshiftTemplate(opts *Options) (crclient.Object, error) { templateParameters := []map[string]string{} templateParameters = append( templateParameters, - map[string]string{"name": openshiftTemplateParams.HyperShiftImage, "value": version.HypershiftImageBase}, - map[string]string{"name": openshiftTemplateParams.HyperShiftImageTag, "value": version.HypershiftImageTag}, + map[string]string{"name": openshiftTemplateParams.HyperShiftImage, "value": fmt.Sprintf("%s:%s", version.HypershiftImageBase, version.HypershiftImageTag)}, + map[string]string{"name": openshiftTemplateParams.HypershiftOperatorReplicas, "value": string(opts.HyperShiftOperatorReplicas)}, map[string]string{"name": openshiftTemplateParams.Namespace, "value": opts.Namespace}, ) + + // oidc S3 parameter if opts.OIDCStorageProviderS3BucketName != "" { templateParameters = append( templateParameters, @@ -151,6 +153,8 @@ func openshiftTemplate(opts *Options) (crclient.Object, error) { map[string]string{"name": openshiftTemplateParams.OIDCS3CredsSecretKey, "value": opts.OIDCStorageProviderS3CredentialsSecretKey}, ) } + + // aws private credentials if opts.AWSPrivateCredentialsSecret != "" { templateParameters = append( templateParameters, @@ -160,6 +164,7 @@ func openshiftTemplate(opts *Options) (crclient.Object, error) { ) } + // external DNS if opts.ExternalDNSProvider != "" && opts.ExternalDNSDomainFilter != "" && opts.ExternalDNSCredentialsSecret != "" { templateParameters = append( templateParameters, @@ -174,8 +179,18 @@ func openshiftTemplate(opts *Options) (crclient.Object, error) { } } - crds, manifests, err := hyperShiftOperatorTemplateManifest(opts, openshiftTemplateParams) - manifests = append(manifests, crds...) + // create manifests + crds, objects, err := hyperShiftOperatorTemplateManifest(opts, openshiftTemplateParams) + objects = append(objects, crds...) + if err != nil { + return nil, err + } + + // patch those manifests, where the template parameter placeholder was not injectable with opts (e.g. type mistmatch) + patches := []ObjectPatch{ + {Kind: "Deployment", Name: "operator", Path: []string{"spec", "replicas"}, Value: openshiftTemplateParams.TemplateParamWrapper(openshiftTemplateParams.HypershiftOperatorReplicas)}, + } + patchedObjects, err := applyPatchesToObjects(objects, patches) if err != nil { return nil, err } @@ -188,13 +203,31 @@ func openshiftTemplate(opts *Options) (crclient.Object, error) { "metadata": map[string]interface{}{ "name": "hypershift-operator-template", }, - "objects": manifests, + "objects": patchedObjects, "parameters": templateParameters, }, } return template, nil } +func applyPatchesToObjects(objects []crclient.Object, patches []ObjectPatch) ([]crclient.Object, error) { + patchedObjects := make([]crclient.Object, len(objects)) + for i, obj := range objects { + content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + patchedObject := &unstructured.Unstructured{Object: content} + for _, p := range patches { + if p.CanBeAppliedTo(patchedObject) { + unstructured.SetNestedField(patchedObject.Object, p.Value, p.Path...) + } + } + patchedObjects[i] = patchedObject + } + return patchedObjects, nil +} + func render(objects []crclient.Object, format string, out io.Writer) error { switch format { case RenderFormatYaml: diff --git a/cmd/install/install_render_test.go b/cmd/install/install_render_test.go index fbf4327f04a..18b0e9fa01b 100644 --- a/cmd/install/install_render_test.go +++ b/cmd/install/install_render_test.go @@ -78,7 +78,7 @@ func TestTemplateYamlRendering(t *testing.T) { t.Fatal("no objects found in template") } params := []string{ - "OPERATOR_REPLICAS", "OPERATOR_IMG", "IMAGE_TAG", "NAMESPACE", + "OPERATOR_REPLICAS", "OPERATOR_IMG", "NAMESPACE", "OIDC_S3_NAME", "OIDC_S3_REGION", "OIDC_S3_CREDS_SECRET", "OIDC_S3_CREDS_SECRET_KEY", } diff --git a/cmd/install/render.go b/cmd/install/render.go index 8a599a778ed..ee7618447b1 100644 --- a/cmd/install/render.go +++ b/cmd/install/render.go @@ -1,30 +1,27 @@ package install import ( - "fmt" - crclient "sigs.k8s.io/controller-runtime/pkg/client" ) type TemplateParams struct { - HyperShiftImage string - HyperShiftImageTag string - Namespace string - OIDCS3Name string - OIDCS3Region string - OIDCS3CredsSecret string - OIDCS3CredsSecretKey string - AWSPrivateRegion string - AWSPrivateCredsSecret string - AWSPrivateCredsSecretKey string - ExternalDNSCredsSecret string - ExternalDNSDomainFilter string - ExternalDNSTxtOwnerID string - ExternalDNSAzureWorkloadIdentity string - ExternalDNSImage string - RegistryOverrides string - TemplateNamespace bool - TemplateParamWrapper func(string) string + HyperShiftImage string + Namespace string + HypershiftOperatorReplicas string + OIDCS3Name string + OIDCS3Region string + OIDCS3CredsSecret string + OIDCS3CredsSecretKey string + AWSPrivateRegion string + AWSPrivateCredsSecret string + AWSPrivateCredsSecretKey string + ExternalDNSCredsSecret string + ExternalDNSDomainFilter string + ExternalDNSTxtOwnerID string + ExternalDNSImage string + RegistryOverrides string + TemplateNamespace bool + TemplateParamWrapper func(string) string } func hyperShiftOperatorTemplateManifest(opts *Options, templateParamConfig TemplateParams) ([]crclient.Object, []crclient.Object, error) { @@ -33,7 +30,7 @@ func hyperShiftOperatorTemplateManifest(opts *Options, templateParamConfig Templ return nil, nil, err } - opts.HyperShiftImage = fmt.Sprintf("%s:%s", templateParamConfig.TemplateParamWrapper(templateParamConfig.HyperShiftImage), templateParamConfig.TemplateParamWrapper(templateParamConfig.HyperShiftImageTag)) + opts.HyperShiftImage = templateParamConfig.TemplateParamWrapper(templateParamConfig.HyperShiftImage) // namespace parameter opts.Namespace = templateParamConfig.TemplateParamWrapper(templateParamConfig.Namespace)