Skip to content

Commit

Permalink
MGMT-15704: Assisted service should create Day2 import CR for hub clu…
Browse files Browse the repository at this point in the history
…ster.

When managing a cluster via ZTP, either through MCE or any other method that results in assisted installer being conmfigured in ZTP mode, we want to import the local cluster into ZTP or which the assisted-operator is running.

The intent is to allow the user to perform day2 operations on the local cluster, to allow the addition of workers and so on.

Presently this is only possible via manual efforts and is not very customer friendly.

This PR aims to resolve this by adding functionality to import the local cluster as described above.
  • Loading branch information
paul-maidment committed Sep 5, 2023
1 parent 52448cd commit 85f69f1
Show file tree
Hide file tree
Showing 8 changed files with 1,159 additions and 1 deletion.
18 changes: 18 additions & 0 deletions cmd/main.go
Expand Up @@ -63,6 +63,7 @@ import (
"github.com/openshift/assisted-service/pkg/generator"
"github.com/openshift/assisted-service/pkg/k8sclient"
"github.com/openshift/assisted-service/pkg/leader"
"github.com/openshift/assisted-service/pkg/localclusterimport"
logconfig "github.com/openshift/assisted-service/pkg/log"
"github.com/openshift/assisted-service/pkg/mirrorregistries"
"github.com/openshift/assisted-service/pkg/ocm"
Expand Down Expand Up @@ -205,6 +206,19 @@ func maxDuration(dur time.Duration, durations ...time.Duration) time.Duration {
return ret
}

func importLocalCluster(ctrlMgr manager.Manager, log *logrus.Logger) {
// Splitting into API reader and writer interfaces as the reader bypasses the cache settings that are on the regular client
// and we need to be able to read secrets that we cannot read with the client due to how the cache is configured.
// The cachedApiClient can be used for writes as these are unaffected by cache.
localClusterImportOperations := localclusterimport.NewLocalClusterImportOperations(ctrlMgr.GetAPIReader(), ctrlMgr.GetClient(), log)
localClusterImport := localclusterimport.NewLocalClusterImport(&localClusterImportOperations, log)
err := localClusterImport.ImportLocalCluster()
if err != nil {
// Failure to import the local cluster is not fatal but we should warn in the log.
log.Warnf("Could not import local cluster into ACM due to error %s", err.Error())
}
}

func main() {
err := envconfig.Process(common.EnvConfigPrefix, &Options)
if err == nil {
Expand Down Expand Up @@ -641,6 +655,10 @@ func main() {
}
}()

if Options.EnableKubeAPI {
importLocalCluster(ctrlMgr, log)
}

// Interrupt servers on SIGINT/SIGTERM
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
Expand Down
7 changes: 7 additions & 0 deletions config/rbac/role.yaml
Expand Up @@ -273,6 +273,12 @@ rules:
- get
- list
- watch
- apiGroups:
- config.openshift.io
resources:
- dnses
verbs:
- get
- apiGroups:
- config.openshift.io
resources:
Expand Down Expand Up @@ -410,6 +416,7 @@ rules:
resources:
- clusterimagesets
verbs:
- create
- get
- list
- watch
Expand Down
Expand Up @@ -618,6 +618,12 @@ spec:
- get
- list
- watch
- apiGroups:
- config.openshift.io
resources:
- dnses
verbs:
- get
- apiGroups:
- config.openshift.io
resources:
Expand Down Expand Up @@ -755,6 +761,7 @@ spec:
resources:
- clusterimagesets
verbs:
- create
- get
- list
- watch
Expand Down
Expand Up @@ -111,10 +111,11 @@ const minimalOpenShiftVersionForDefaultNetworkTypeOVNKubernetes = "4.12.0-0.0"
// +kubebuilder:rbac:groups=hive.openshift.io,resources=clusterdeployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=hive.openshift.io,resources=clusterdeployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=hive.openshift.io,resources=clusterdeployments/finalizers,verbs=update
// +kubebuilder:rbac:groups=hive.openshift.io,resources=clusterimagesets,verbs=get;list;watch
// +kubebuilder:rbac:groups=hive.openshift.io,resources=clusterimagesets,verbs=get;list;watch;create
// +kubebuilder:rbac:groups=extensions.hive.openshift.io,resources=agentclusterinstalls,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=extensions.hive.openshift.io,resources=agentclusterinstalls/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=extensions.hive.openshift.io,resources=agentclusterinstalls/finalizers,verbs=update
// +kubebuilder:rbac:groups=config.openshift.io,resources=dnses,verbs=get

func (r *ClusterDeploymentsReconciler) Reconcile(origCtx context.Context, req ctrl.Request) (ctrl.Result, error) {
ctx := addRequestIdIfNeeded(origCtx)
Expand Down
228 changes: 228 additions & 0 deletions pkg/localclusterimport/import_local_cluster.go
@@ -0,0 +1,228 @@
package localclusterimport

import (
"encoding/base64"
"errors"
"fmt"

configv1 "github.com/openshift/api/config/v1"
hiveext "github.com/openshift/assisted-service/api/hiveextension/v1beta1"
hivev1 "github.com/openshift/hive/apis/hive/v1"
"github.com/openshift/hive/apis/hive/v1/agent"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const LocalClusterNamespace = "local-cluster"

type LocalClusterImport struct {
clusterImportOperations ClusterImportOperations
log *logrus.Logger
}

func NewLocalClusterImport(localClusterImportOperations ClusterImportOperations, log *logrus.Logger) LocalClusterImport {
return LocalClusterImport{clusterImportOperations: localClusterImportOperations, log: log}
}

func (i *LocalClusterImport) shouldImportLocalCluster() (bool, error) {
// The presence of agentClusterInstall for the local cluster will determine whether or not we need to perform an import
agentClusterInstall, err := i.clusterImportOperations.GetAgentClusterInstall(LocalClusterNamespace, LocalClusterNamespace+"-cluster-install")
if err != nil && !k8serrors.IsNotFound(err) {
i.log.Errorf("hubClusterDay2Importer: There was an error fetching the AgentClusterInstall while checking for need to register local cluster.")
return false, err
}
// TODO: We need to determine what to do if there has been a recent upgrade
// See MGMT-15705 for more information.
if agentClusterInstall != nil {
i.log.Infof("hubClusterDay2Importer: Found AgentClusterInstall for hub cluster, assuming that hub has been correctly registered for ZTP day 2 operations: %s", agentClusterInstall.Name)
return false, nil
}
return true, nil
}

func (i *LocalClusterImport) ImportLocalCluster() error {
clusterVersion, err := i.clusterImportOperations.GetClusterVersion("version")
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Unable to find cluster version info due to error: %s", err.Error())
return err
}

shouldImportLocalCluster, err := i.shouldImportLocalCluster()
if err != nil {
return err
}
if !shouldImportLocalCluster {
message := "hubClusterDay2Importer: no need to import local cluster as registration already detected"
i.log.Info(message)
return nil
}

release_image := ""
if clusterVersion == nil || clusterVersion.Status.History[0].State != configv1.CompletedUpdate {
message := "hubClusterDay2Importer: Cluster version info is empty, cannot proceed"
i.log.Error(message)
return errors.New(message)
}
release_image = clusterVersion.Status.History[0].Image

kubeConfigSecret, err := i.clusterImportOperations.GetSecret("openshift-kube-apiserver", "node-kubeconfigs")
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Unable to fetch local cluster kubeconfigs due to error %s", err.Error())
return err
}
i.log.Infof("hubClusterDay2Importer: Found secret %s", kubeConfigSecret.Name)

pullSecret, err := i.clusterImportOperations.GetSecret("openshift-machine-api", "pull-secret")
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Unable to fetch pull secret due to error %s", err.Error())
return err
}
i.log.Infof("hubClusterDay2Importer: Found secret %s", pullSecret.Name)


dns, err := i.clusterImportOperations.GetDNS()
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Could not fetch DNS due to error %s", err.Error())
return err
}

numberOfControlPlaneNodes, err := i.clusterImportOperations.GetNumberOfControlPlaneNodes()
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Unable to determine the number of control plane nodes due to error %s", err.Error())
return err
}
i.log.Infof("hubClusterDay2Importer: Number of control plane nodes is %d", numberOfControlPlaneNodes)

clusterImageSet := hivev1.ClusterImageSet{
ObjectMeta: metav1.ObjectMeta{
Name: "local-cluster-image-set",
},
Spec: hivev1.ClusterImageSetSpec{
ReleaseImage: release_image,
},
}
_, err = i.clusterImportOperations.CreateClusterImageSet(&clusterImageSet)
if err != nil {
i.log.Errorf("hubClusterDay2Importer: unable to create ClusterImageSet due to error: %s", err.Error())
return err
}

// Store the kubeconfig data in the local cluster namespace
localClusterSecret := v1.Secret{}
localClusterSecret.Name = fmt.Sprintf("%s-admin-kubeconfig", LocalClusterNamespace)
localClusterSecret.Namespace = LocalClusterNamespace
localClusterSecret.Data = make(map[string][]byte)
localClusterSecret.Data["kubeconfig"] = kubeConfigSecret.Data["lb-ext.kubeconfig"]
kubeConfigSecret, err = i.clusterImportOperations.CreateSecret(LocalClusterNamespace, &localClusterSecret)
if err != nil {
i.log.Errorf("hubClusterDay2Importer: to store secret due to error %s", err.Error())
return err
}
i.log.Infof("hubClusterDay2Importer: Created secret %s", localClusterSecret.Name)

// Store the pull secret in the local cluster namespace
hubPullSecret := v1.Secret{}
hubPullSecret.Name = pullSecret.Name
hubPullSecret.Namespace = LocalClusterNamespace
hubPullSecret.Data = make(map[string][]byte)
// .dockerconfigjson is double base64 encoded for some reason.
// simply obtaining the secret above will perform one layer of decoding.
// we need to manually perform another to ensure that the data is correctly copied to the new secret.
hubPullSecret.Data[".dockerconfigjson"], err = base64.StdEncoding.DecodeString(string(pullSecret.Data[".dockerconfigjson"]))
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Unable to decode base64 pull secret data due to error %s", err.Error())
return err
}
hubPullSecret.OwnerReferences = []metav1.OwnerReference{}
_, err = i.clusterImportOperations.CreateSecret(LocalClusterNamespace, &hubPullSecret)
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Unable to store hub pull secret due to error %s", err.Error())
return err
}

//Create an AgentClusterInstall in the local cluster namespace
userManagedNetworkingActive := true
agentClusterInstall := &hiveext.AgentClusterInstall{
Spec: hiveext.AgentClusterInstallSpec{
Networking: hiveext.Networking{
UserManagedNetworking: &userManagedNetworkingActive,
},
ClusterDeploymentRef: v1.LocalObjectReference{
Name: LocalClusterNamespace + "-cluster-deployment",
},
ImageSetRef: &hivev1.ClusterImageSetReference{
Name: "local-cluster-image-set",
},
ProvisionRequirements: hiveext.ProvisionRequirements{
ControlPlaneAgents: numberOfControlPlaneNodes,
},
},
}
agentClusterInstall.Namespace = LocalClusterNamespace
agentClusterInstall.Name = LocalClusterNamespace + "-cluster-install"
err = i.clusterImportOperations.CreateAgentClusterInstall(agentClusterInstall)
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Could not create AgentClusterInstall due to error %s", err.Error())
return err
}

// Fetch the recently stored AgentClusterInstall so that we can obtain the UID
aci, err := i.clusterImportOperations.GetAgentClusterInstall(LocalClusterNamespace, LocalClusterNamespace+"-cluster-install")
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Failed to fetch created AgentClusterInstall due to error %s", err.Error())
return err
}

// Create a cluster deployment in the local cluster namespace
clusterDeployment := &hivev1.ClusterDeployment{
Spec: hivev1.ClusterDeploymentSpec{
Installed: true, // Let ZTP know to import this cluster
ClusterMetadata: &hivev1.ClusterMetadata{
ClusterID: "",
InfraID: "",
AdminKubeconfigSecretRef: v1.LocalObjectReference{Name: kubeConfigSecret.Name},
},
ClusterInstallRef: &hivev1.ClusterInstallLocalReference{
Name: LocalClusterNamespace + "-cluster-install",
Group: "extensions.hive.openshift.io",
Kind: "AgentClusterInstall",
Version: "v1beta1",
},
Platform: hivev1.Platform{
AgentBareMetal: &agent.BareMetalPlatform{
AgentSelector: metav1.LabelSelector{
MatchLabels: map[string]string{"infraenv": "local-cluster"},
},
},
},
PullSecretRef: &v1.LocalObjectReference{
Name: pullSecret.Name,
},
},
}
clusterDeployment.Name = LocalClusterNamespace + "-cluster-deployment"
clusterDeployment.Namespace = LocalClusterNamespace
clusterDeployment.Spec.ClusterName = LocalClusterNamespace + "-cluster-deployment"
clusterDeployment.Spec.BaseDomain = dns.Spec.BaseDomain
//
// Adding this ownership reference to ensure we can submit clusterDeployment without ManagedClusterSet/join permission
//
// Must match https://github.com/stolostron/multicloud-operators-foundation/blob/0001f46a5115fe43c606a068e5e7ee00abec3b68/pkg/webhook/clusterset/validatingWebhook.go#L44
// or the ClusterDeployment will be rejected during admission.
agentClusterInstallOwnerRef := metav1.OwnerReference{
Kind: "AgentCluster",
APIVersion: "capi-provider.agent-install.openshift.io/v1alpha1",
Name: LocalClusterNamespace + "-cluster-install",
UID: aci.UID,
}
clusterDeployment.OwnerReferences = []metav1.OwnerReference{agentClusterInstallOwnerRef}
_, err = i.clusterImportOperations.CreateClusterDeployment(clusterDeployment)
if err != nil {
i.log.Errorf("hubClusterDay2Importer: Could not create ClusterDeployment due to error %s", err.Error())
return err
}
i.log.Info("hubClusterDay2Importer: Completed Day2 import of hub cluster")
return nil
}

0 comments on commit 85f69f1

Please sign in to comment.