diff --git a/data/data/manifests/bootkube/internal-release-image-tls-secret.yaml.template b/data/data/manifests/bootkube/internal-release-image-tls-secret.yaml.template new file mode 100644 index 0000000000..fdb85391ba --- /dev/null +++ b/data/data/manifests/bootkube/internal-release-image-tls-secret.yaml.template @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Secret +metadata: + name: internal-release-image-tls + namespace: openshift-machine-config-operator + annotations: + openshift.io/description: Secret containing the InternalReleaseImage registry TLS certificate and key + openshift.io/owning-component: Machine Config Operator + labels: + auth.openshift.io/managed-certificate-type: target +type: kubernetes.io/tls +data: + tls.crt: {{.IriTLSCert}} + tls.key: {{.IriTLSKey}} diff --git a/pkg/asset/ignition/bootstrap/common.go b/pkg/asset/ignition/bootstrap/common.go index 4dc9ac94a8..1ea2d88994 100644 --- a/pkg/asset/ignition/bootstrap/common.go +++ b/pkg/asset/ignition/bootstrap/common.go @@ -168,6 +168,7 @@ func (a *Common) Dependencies() []asset.Asset { &tls.KubeletCSRSignerCertKey{}, &tls.KubeletServingCABundle{}, &tls.MCSCertKey{}, + &tls.IRICertKey{}, &tls.RootCA{}, &tls.ServiceAccountKeyPair{}, &tls.IronicTLSCert{}, @@ -670,6 +671,7 @@ func (a *Common) addParentFiles(dependencies asset.Parents) { &tls.KubeletCSRSignerCertKey{}, &tls.KubeletServingCABundle{}, &tls.MCSCertKey{}, + &tls.IRICertKey{}, &tls.ServiceAccountKeyPair{}, &tls.JournalCertKey{}, &tls.IronicTLSCert{}, diff --git a/pkg/asset/manifests/operators.go b/pkg/asset/manifests/operators.go index da7abccc19..9e35458da7 100644 --- a/pkg/asset/manifests/operators.go +++ b/pkg/asset/manifests/operators.go @@ -16,6 +16,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/yaml" + "github.com/openshift/api/features" "github.com/openshift/installer/pkg/asset" "github.com/openshift/installer/pkg/asset/installconfig" "github.com/openshift/installer/pkg/asset/rhcos" @@ -76,6 +77,8 @@ func (m *Manifests) Dependencies() []asset.Asset { &ImageDigestMirrorSet{}, &tls.RootCA{}, &tls.MCSCertKey{}, + &tls.IRICertKey{}, + &manifests.InternalReleaseImage{}, new(rhcos.Image), &bootkube.CVOOverrides{}, @@ -85,6 +88,7 @@ func (m *Manifests) Dependencies() []asset.Asset { &bootkube.MachineConfigServerCAConfigMap{}, &bootkube.MachineConfigServerTLSSecret{}, &bootkube.OpenshiftConfigSecretPullSecret{}, + &bootkube.InternalReleaseImageTLSSecret{}, &BMCVerifyCAConfigMap{}, } } @@ -224,9 +228,42 @@ func (m *Manifests) generateBootKubeManifests(dependencies asset.Parents) []*ass }) } } + + if installConfig.Config.EnabledFeatureGates().Enabled(features.FeatureGateNoRegistryClusterInstall) { + iri := &manifests.InternalReleaseImage{} + dependencies.Get(iri) + + // Skip if InternalReleaseImage manifest wasn't found. + if len(iri.FileList) > 0 { + files = append(files, appendIRIcerts(dependencies)) + } + } + return files } +func appendIRIcerts(dependencies asset.Parents) *asset.File { + iriCertKey := &tls.IRICertKey{} + iriTLSSecret := &bootkube.InternalReleaseImageTLSSecret{} + dependencies.Get(iriCertKey, iriTLSSecret) + + f := iriTLSSecret.Files()[0] + + templateData := struct { + IriTLSCert string + IriTLSKey string + }{ + IriTLSCert: base64.StdEncoding.EncodeToString(iriCertKey.Cert()), + IriTLSKey: base64.StdEncoding.EncodeToString(iriCertKey.Key()), + } + fileData := applyTemplateData(f.Data, templateData) + + return &asset.File{ + Filename: path.Join(manifestDir, strings.TrimSuffix(filepath.Base(f.Filename), ".template")), + Data: fileData, + } +} + func applyTemplateData(data []byte, templateData interface{}) []byte { template := template.Must(template.New("template").Funcs(customTmplFuncs).Parse(string(data))) buf := &bytes.Buffer{} diff --git a/pkg/asset/store/assetcreate_test.go b/pkg/asset/store/assetcreate_test.go index c823f36283..6ca84e2878 100644 --- a/pkg/asset/store/assetcreate_test.go +++ b/pkg/asset/store/assetcreate_test.go @@ -124,6 +124,7 @@ func TestCreatedAssetsAreNotDirty(t *testing.T) { "Cluster API Machine Manifests": true, // no files for the 'none' platform and ClusterAPIInstall feature gate not set "Metadata": true, // read-only "Kubeadmin Password": true, // read-only + "InternalReleaseImageTLSSecret": true, // no files when NoRegistryClusterInstall feature gate is not set } for _, a := range tc.targets { name := a.Name() diff --git a/pkg/asset/targets/targets.go b/pkg/asset/targets/targets.go index de6433bd6a..b1fc677585 100644 --- a/pkg/asset/targets/targets.go +++ b/pkg/asset/targets/targets.go @@ -48,6 +48,7 @@ var ( &openshift.KubeadminPasswordSecret{}, &openshift.RoleCloudCredsSecretReader{}, &openshift.AzureCloudProviderSecret{}, + &bootkube.InternalReleaseImageTLSSecret{}, } // IgnitionConfigs are the ignition-configs targeted assets. diff --git a/pkg/asset/templates/content/bootkube/internal-release-image-server-tls-secret.go b/pkg/asset/templates/content/bootkube/internal-release-image-server-tls-secret.go new file mode 100644 index 0000000000..d11952ae32 --- /dev/null +++ b/pkg/asset/templates/content/bootkube/internal-release-image-server-tls-secret.go @@ -0,0 +1,85 @@ +package bootkube + +import ( + "context" + "os" + "path/filepath" + + "github.com/openshift/api/features" + "github.com/openshift/installer/pkg/asset" + "github.com/openshift/installer/pkg/asset/installconfig" + "github.com/openshift/installer/pkg/asset/templates/content" + "github.com/openshift/installer/pkg/asset/templates/content/manifests" +) + +const ( + internalReleaseImageTLSSecretFileName = "internal-release-image-tls-secret.yaml.template" +) + +var _ asset.WritableAsset = (*InternalReleaseImageTLSSecret)(nil) + +// InternalReleaseImageTLSSecret is the constant to represent contents of machine_configservertlssecret.yaml.template file. +type InternalReleaseImageTLSSecret struct { + FileList []*asset.File +} + +// Dependencies returns all of the dependencies directly needed by the asset. +func (t *InternalReleaseImageTLSSecret) Dependencies() []asset.Asset { + return []asset.Asset{ + &installconfig.InstallConfig{}, + &manifests.InternalReleaseImage{}, + } +} + +// Name returns the human-friendly name of the asset. +func (t *InternalReleaseImageTLSSecret) Name() string { + return "InternalReleaseImageTLSSecret" +} + +// Generate generates the actual files by this asset. +func (t *InternalReleaseImageTLSSecret) Generate(_ context.Context, dependencies asset.Parents) error { + installConfig := &installconfig.InstallConfig{} + iri := &manifests.InternalReleaseImage{} + + dependencies.Get(installConfig, iri) + + if !installConfig.Config.EnabledFeatureGates().Enabled(features.FeatureGateNoRegistryClusterInstall) { + return nil + } + + // Skip if InternalReleaseImage manifest wasn't found. + if len(iri.FileList) == 0 { + return nil + } + + fileName := internalReleaseImageTLSSecretFileName + data, err := content.GetBootkubeTemplate(fileName) + if err != nil { + return err + } + t.FileList = []*asset.File{ + { + Filename: filepath.Join(content.TemplateDir, fileName), + Data: data, + }, + } + return nil +} + +// Files returns the files generated by the asset. +func (t *InternalReleaseImageTLSSecret) Files() []*asset.File { + return t.FileList +} + +// Load returns the asset from disk. +func (t *InternalReleaseImageTLSSecret) Load(f asset.FileFetcher) (bool, error) { + file, err := f.FetchByName(filepath.Join(content.TemplateDir, internalReleaseImageTLSSecretFileName)) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + t.FileList = []*asset.File{file} + return true, nil +} diff --git a/pkg/asset/templates/content/manifests/internalreleaseimage.go b/pkg/asset/templates/content/manifests/internalreleaseimage.go new file mode 100644 index 0000000000..f5a8931bbb --- /dev/null +++ b/pkg/asset/templates/content/manifests/internalreleaseimage.go @@ -0,0 +1,79 @@ +package manifests + +import ( + "context" + "path/filepath" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" + + "github.com/openshift/installer/pkg/asset" +) + +const ( + internalReleaseImageKind = "InternalReleaseImage" + internalReleaseImageInstanceName = "cluster" + openshiftManifestDir = "openshift" +) + +// InternalReleaseImage simply checks for the presence of the related manifest. +type InternalReleaseImage struct { + FileList []*asset.File +} + +var ( + _ asset.WritableAsset = (*InternalReleaseImage)(nil) +) + +// Name returns a human friendly name for the operator. +func (iri *InternalReleaseImage) Name() string { + return "InternalReleaseImage manifest" +} + +// Dependencies returns all of the dependencies required by the asset. +func (iri *InternalReleaseImage) Dependencies() []asset.Asset { + return []asset.Asset{} +} + +// Generate is not required for InternalReleaseImage. +func (iri *InternalReleaseImage) Generate(_ context.Context, dependencies asset.Parents) error { + return nil +} + +// Files returns the files generated by the asset. +func (iri *InternalReleaseImage) Files() []*asset.File { + return iri.FileList +} + +// Load reads the asset files from disk. +func (iri *InternalReleaseImage) Load(f asset.FileFetcher) (found bool, err error) { + files := []*asset.File{} + yamlFiles, err := f.FetchByPattern(filepath.Join(openshiftManifestDir, "*.yaml")) + if err != nil { + return false, errors.Wrap(err, "failed to load *.yaml files") + } + files = append(files, yamlFiles...) + + ymlFileList, err := f.FetchByPattern(filepath.Join(openshiftManifestDir, "*.yml")) + if err != nil { + return false, errors.Wrap(err, "failed to load *.yml files") + } + files = append(files, ymlFileList...) + + for _, f := range files { + u := &unstructured.Unstructured{} + if err := yaml.Unmarshal(f.Data, u); err != nil { + logrus.Warnf("failed to unmarshal file %s: %v", f.Filename, err) + continue + } + + if u.GetKind() == internalReleaseImageKind && u.GetName() == internalReleaseImageInstanceName { + iri.FileList = append(iri.FileList, f) + break + } + } + + return len(iri.FileList) > 0, nil +} diff --git a/pkg/asset/tls/iricertkey.go b/pkg/asset/tls/iricertkey.go new file mode 100644 index 0000000000..387f7e6ee9 --- /dev/null +++ b/pkg/asset/tls/iricertkey.go @@ -0,0 +1,95 @@ +package tls + +import ( + "context" + "crypto/x509" + "crypto/x509/pkix" + "net" + + features "github.com/openshift/api/features" + "github.com/openshift/installer/pkg/asset" + "github.com/openshift/installer/pkg/asset/installconfig" + "github.com/openshift/installer/pkg/asset/templates/content/manifests" + baremetaltypes "github.com/openshift/installer/pkg/types/baremetal" + nutanixtypes "github.com/openshift/installer/pkg/types/nutanix" + vspheretypes "github.com/openshift/installer/pkg/types/vsphere" +) + +// IRICertKey is the asset that generates the InternalReleaseImage registry key/cert pair. +type IRICertKey struct { + SignedCertKey +} + +var _ asset.Asset = (*IRICertKey)(nil) + +// Dependencies returns the dependency of the the cert/key pair, which includes +// the parent CA, and install config if it depends on the install config for +// DNS names, etc. +func (a *IRICertKey) Dependencies() []asset.Asset { + return []asset.Asset{ + &RootCA{}, + &installconfig.InstallConfig{}, + &manifests.InternalReleaseImage{}, + } +} + +// Generate generates the cert/key pair based on its dependencies. +func (a *IRICertKey) Generate(ctx context.Context, dependencies asset.Parents) error { + ca := &RootCA{} + installConfig := &installconfig.InstallConfig{} + iri := &manifests.InternalReleaseImage{} + dependencies.Get(ca, installConfig, iri) + + if !installConfig.Config.EnabledFeatureGates().Enabled(features.FeatureGateNoRegistryClusterInstall) { + return nil + } + + // Skip if InternalReleaseImage manifest wasn't found. + if len(iri.FileList) == 0 { + return nil + } + + apiInt := internalAPIAddress(installConfig.Config) + + cfg := &CertCfg{ + Subject: pkix.Name{CommonName: "system:internal-release-image"}, + ExtKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + Validity: ValidityTenYears(), + } + + var vips []string + switch installConfig.Config.Platform.Name() { + case baremetaltypes.Name: + vips = installConfig.Config.BareMetal.APIVIPs + case nutanixtypes.Name: + vips = installConfig.Config.Nutanix.APIVIPs + case vspheretypes.Name: + vips = installConfig.Config.VSphere.APIVIPs + } + + cfg.IPAddresses = []net.IP{} + cfg.DNSNames = []string{ + "localhost", + apiInt, + } + localIPs := []string{ + "127.0.0.1", + "::1", + } + for _, vip := range vips { + cfg.IPAddresses = append(cfg.IPAddresses, net.ParseIP(vip)) + cfg.DNSNames = append(cfg.DNSNames, vip) + } + for _, i := range localIPs { + if ip := net.ParseIP(i); ip != nil { + cfg.IPAddresses = append(cfg.IPAddresses, ip) + } + } + + return a.SignedCertKey.Generate(ctx, cfg, ca, "internal-release-image", DoNotAppendParent) +} + +// Name returns the human-friendly name of the asset. +func (a *IRICertKey) Name() string { + return "Certificate (InternalReleaseImage)" +}