Skip to content

Commit

Permalink
Add controller which reports K8s and OCP version to control-api
Browse files Browse the repository at this point in the history
The controller reconciles clusterversion.config.openshift.io which
should be sufficient to detect changes in the K8s/OCP version.
  • Loading branch information
simu committed Nov 12, 2024
1 parent 3656c75 commit 55cc6f4
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 5 deletions.
146 changes: 146 additions & 0 deletions controllers/zonek8sversion_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package controllers

import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"unicode"

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/record"

controlv1 "github.com/appuio/control-api/apis/v1"
configv1 "github.com/openshift/api/config/v1"

"go.uber.org/multierr"
)

type ZoneK8sVersionReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder

ForeignClient client.Client
RESTClient rest.Interface

// upstream zone ID. The agent expects that the control-api zone
// object is labeled with
ZoneID string
}

const (
upstreamZoneIdentifierLabelKey = "control.appuio.io/zone-cluster-id"
kubernetesVersionFeatureKey = "kubernetesVersion"
openshiftVersionFeatureKey = "openshiftVersion"
)

func extractOpenShiftVersion(cv *configv1.ClusterVersion) string {
currentVersion := ""
lastUpdate := time.Time{}
for _, h := range cv.Status.History {
if h.State == "Completed" && h.Verified == true && h.CompletionTime.Time.After(lastUpdate) {
currentVersion = h.Version
lastUpdate = h.CompletionTime.Time
}
}
if currentVersion == "" {
return cv.Status.Desired.Version
}
return currentVersion
}

func extractK8sVersion(v *version.Info) (string, error) {
major := trimVersion(v.Major)
if major == "" {
return "", fmt.Errorf("unknown major version %q", v.Major)
}
minor := trimVersion(v.Minor)
if minor == "" {
return "", fmt.Errorf("unknown minor version %q", v.Minor)
}
gitverparts := strings.Split(v.GitVersion, ".")
patchV := strings.Split(gitverparts[2], "+")[0]
patch := trimVersion(patchV)
if patch == "" {
return "", fmt.Errorf("unknown patch version %q", patchV)
}
return fmt.Sprintf("%s.%s.%s", major, minor, patch), nil
}

func trimVersion(v string) string {
res := []rune{}
for _, r := range v {
if !unicode.IsDigit(r) {
break
}
res = append(res, r)
}
return string(res)
}

// Reconcile reads the K8s and OCP versions and writes them to the upstream
// zone
func (r *ZoneK8sVersionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
l.Info("Reconciling zone K8s version")

var cv = configv1.ClusterVersion{}
if err := r.Client.Get(ctx, req.NamespacedName, &cv); err != nil {
return ctrl.Result{}, err
}

ocpVer := extractOpenShiftVersion(&cv)

l.Info("OCP current version", "version", ocpVer)

body, err := r.RESTClient.Get().AbsPath("/version").Do(ctx).Raw()
if err != nil {
return ctrl.Result{}, err
}
var info version.Info
err = json.Unmarshal(body, &info)
if err != nil {
return ctrl.Result{}, err
}
k8sVer, err := extractK8sVersion(&info)
if err != nil {
return ctrl.Result{}, err
}

l.Info("K8s current version", "version", k8sVer)

var zones = controlv1.ZoneList{}
if err := r.ForeignClient.List(ctx, &zones, client.MatchingLabels{upstreamZoneIdentifierLabelKey: r.ZoneID}); err != nil {
return ctrl.Result{}, err
}
var errs []error
for _, z := range zones.Items {
if k8sVer != "" {
z.Data.Features[kubernetesVersionFeatureKey] = k8sVer
}
if ocpVer != "" {
z.Data.Features[openshiftVersionFeatureKey] = ocpVer
}
if err := r.ForeignClient.Update(ctx, &z); err != nil {
errs = append(errs, err)
}
}

return ctrl.Result{}, multierr.Combine(errs...)
}

// SetupWithManager sets up the controller with the Manager.
func (r *ZoneK8sVersionReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&configv1.ClusterVersion{}).
Named("zone_k8s_version").
Complete(r)
}
37 changes: 32 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cluster"
Expand All @@ -33,6 +34,8 @@ import (
"github.com/appuio/appuio-cloud-agent/skipper"
"github.com/appuio/appuio-cloud-agent/webhooks"
whoamicli "github.com/appuio/appuio-cloud-agent/whoami"

configv1 "github.com/openshift/api/config/v1"
)

var (
Expand All @@ -56,6 +59,7 @@ func init() {
utilruntime.Must(projectv1.AddToScheme(scheme))
utilruntime.Must(agentv1.AddToScheme(scheme))
utilruntime.Must(controlv1.AddToScheme(scheme))
utilruntime.Must(configv1.AddToScheme(scheme))
//+kubebuilder:scaffold:scheme
}

Expand All @@ -77,7 +81,7 @@ func main() {
flag.StringVar(&controlAPIURL, "control-api-url", "", "URL of the control API. If set agent does not use `-kubeconfig-control-api`. Expects a bearer token in `CONTROL_API_BEARER_TOKEN` env var.")

var upstreamZoneIdentifier string
flag.StringVar(&upstreamZoneIdentifier, "upstream-zone-identifier", "", "Identifies the agent in the control API. Currently used for Team/OrganizationMembers finalizer. Must be set if the GroupSync controller is enabled.")
flag.StringVar(&upstreamZoneIdentifier, "upstream-zone-identifier", "", "Identifies the agent in the control API. Currently used for Team/OrganizationMembers finalizer and the K8s version reporting.")

var selectedUsageProfile string
flag.StringVar(&selectedUsageProfile, "usage-profile", "", "UsageProfile to use. Applies all profiles if empty. Dynamic selection is not supported yet.")
Expand Down Expand Up @@ -175,8 +179,14 @@ func main() {
os.Exit(1)
}

if upstreamZoneIdentifier == "" {
setupLog.Error(err, "upstream-zone-identifier must be set.")
os.Exit(1)
}

registerRatioController(mgr, conf, conf.OrganizationLabel)
registerOrganizationRBACController(mgr, conf.OrganizationLabel, conf.DefaultOrganizationClusterRoles)
registerZoneK8sVersionController(mgr, controlAPICluster, upstreamZoneIdentifier)

if !disableUserAttributeSync {
if err := (&controllers.UserAttributeSyncReconciler{
Expand All @@ -191,10 +201,6 @@ func main() {
}
}
if !disableGroupSync {
if upstreamZoneIdentifier == "" {
setupLog.Error(err, "upstream-zone-identifier must be set if GroupSync controller is enabled")
os.Exit(1)
}
if err := (&controllers.GroupSyncReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand Down Expand Up @@ -452,3 +458,24 @@ func registerRatioController(mgr ctrl.Manager, conf Config, orgLabel string) {
os.Exit(1)
}
}

func registerZoneK8sVersionController(mgr ctrl.Manager, controlAPICluster cluster.Cluster, upstreamZoneIdentifier string) {
restclient, err := kubernetes.NewForConfig(mgr.GetConfig())
if err != nil {
setupLog.Error(err, "unable to create clientset for config", "controller", "zone-k8s-version")
os.Exit(1)
}
if err := (&controllers.ZoneK8sVersionReconciler{
Client: mgr.GetClient(),
RESTClient: restclient.RESTClient(),
Recorder: mgr.GetEventRecorderFor("zone-k8s-version-controller"),
Scheme: mgr.GetScheme(),

ForeignClient: controlAPICluster.GetClient(),

ZoneID: upstreamZoneIdentifier,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "zone-k8s-version")
os.Exit(1)
}
}

0 comments on commit 55cc6f4

Please sign in to comment.